Sep 30 2009

Using jQuery UI to make a drag-and-drop capable table

Category: JavaScriptMatt @ 01:30

I’m adding support for dragging and dropping rows to jQuery, and as you can probably tell from reading this blog, I’m lazy.  I don’t want to reinvent the wheel, so I was not very keen on the idea of trying to implement dragging from scratch.  jQuery UI to the rescue!  But some quick Googling had me discouraged.  I couldn’t find any examples where people had successfully used the draggable and droppable plugins in the manner I wanted.  Ugh. 

Still, I decided to give it a shot, and low-and-behold, I got it working!  The hard part was figuring out some of the options because the jQuery UI documentation is not very helpful.  The actual code was quite simple.

CSS:

<style type="text/css">        
    tr.draggable > td > div.ui-state-default 
    {
        cursor: move;
    }
    
    .ghost 
    {
        background-color: Gray;
        opacity: 0.5;
    }
</style>

Markup:

<table>
    <tr class="draggable droppable">
        <td><div class="ui-state-default ui-corner-all"><span class="ui-icon ui-icon-grip-dotted-vertical"/></div></td>
        <td>Test 1</td>
        <td>Test 1</td>
    </tr>
    <tr class="draggable droppable">
        <td><div class="ui-state-default ui-corner-all"><span class="ui-icon ui-icon-grip-dotted-vertical"/></div></td>
        <td>Test 2</td>
        <td>Test 2</td>
    </tr>
    <tr class="draggable droppable">
        <td><div class="ui-state-default ui-corner-all"><span class="ui-icon ui-icon-grip-dotted-vertical"/></div></td>
        <td>Test 3</td>
        <td>Test 3</td>
    </tr>
    <tr class="draggable droppable">
        <td><div class="ui-state-default ui-corner-all"><span class="ui-icon ui-icon-grip-dotted-vertical"/></div></td>
        <td>Test 4</td>
        <td>Test 4</td>
    </tr>
</table>

And finally script:

$(".draggable").draggable(
{
    helper: function() { return "<div class='ghost'></div>"; },
    start: resizeGhost,
    revert: 'invalid',
    handle: 'span'
});

function resizeGhost(event, ui) {
    var helper = ui.helper;
    var element = $(event.target);
    helper.width(element.width());
    helper.height(element.height());
}

$(".droppable").droppable({
    hoverClass: 'ui-state-active',
    drop: function(event, ui) {
        var target = $(event.target);
        var draggable = ui.draggable;

        draggable.insertBefore(target);
    }
});

Note that most of the jQuery UI classes I’ve applied are not required, I just used them to get a nice drag handle.

Tags:

Aug 5 2009

Internet Explorer 8 table-layout bug

Category: JavaScript | jQueryMatt @ 05:36

If you’ve been following me on Twitter, you no doubt know that I’m fighting rendering problems with Internet Explorer 8.  Today, I have finally tracked down one very elusive bug.  The problem occurs in liteGrid when a user clicks on a cell to edit it.  The contents of the cell are placed with an input element.  When the user is finished, they can toggle out of edit mode by pressing escape, enter, or changing the focus to a different element, at which point the input is removed and replaced with the new value.  In IE8, the cell width will randomly lose 3 pixels of width when the contents of the cell is changed.  Sometimes it occurs when entering edit mode, sometimes it occurs after exiting.  Not surprisingly, it works fine in both Chrome and Firefox. 

After much debugging, I came up with a bare bones reproduction case:

<script type="text/javascript" src="http://www.google.com/jsapi"></script>
<script type="text/javascript">
  
  google.load("jquery", "1.3.2");
 
  google.setOnLoadCallback(function() {
        $("#buggedTable td").click(function(){
                var el = $(this);
                
                if (el.text() == "New value") {
                    el.text("Newer value");
                }
                else {
                    el.text("New value");
                }
        });
    });
</script>
...
<style type="text/css">

    #buggedTable 
    {
        table-layout: fixed;
        border-collapse: collapse;
        border: 1px solid black;
        width: 100%;
    }
    
    #buggedTable td, th 
    {
        border: 1px solid black;
        overflow: hidden;
        white-space: nowrap;
    }
        
    #buggedTable td > input
    {
        max-width: 99%;
        display: inline;
        margin: 0;
        padding: 0;
    }
    
</style>
...
<table id="buggedTable">
    <colgroup>
        <col style="width: 100px;" />
        <col style="width: 100px;" />
        <col style="width: 100px;" />
        <col style="width: auto;" />
    </colgroup>
    <thead>
        <tr>
            <th>Col1</th>
            <th>Col2</th>
            <th>Col3</th>
            <th>Col4</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Value 1-1Value 1-1Value 1-1</td>
            <td>Value 1-2</td>
            <td>Value 1-3</td>
            <td>Value 1-4</td>
        </tr>
        <tr>
            <td>Value 2-1</td>
            <td>Value 2-2</td>
            <td>Value 2-3</td>
            <td>Value 2-4</td>
        </tr>
        <tr>
            <td>Value 3-1</td>
            <td>Value 3-2</td>
            <td>Value 3-3</td>
            <td>Value 3-4</td>
        </tr>
        <tr>
            <td>Value 4-1</td>
            <td>Value 4-2</td>
            <td>Value 4-3</td>
            <td>Value 4-4</td>
        </tr>
        <tr>
            <td>Value 5-1</td>
            <td>Value 5-2</td>
            <td>Value 5-3</td>
            <td>Value 5-4</td>
        </tr>
    </tbody>
</table>

Try that code out here with Internet Explorer 8 and see what you think.  Just randomly click on cells like a mad man until you get it to misbehave.  Eventually, you should see something like this:

TableLayoutBut

It’s subtle, but you can probably see that the lines between the columns no longer line up correctly on a few of the cells.

The culprit is the “table-layout:fixed” style.  Without that style, the table cells do not resize.  Unfortunately, liteGrid has to have that functionality, otherwise columns can’t be resized to be smaller than the longest cell value. 

But Matt, you are using jQuery!  Couldn’t it be a jQuery bug?!?”

No. 

Are you sure?”

Yes.

Oh yeah?!?  PROVE IT!”

Alright, so I can’t prove it, but I can demonstrate the bug without using JavaScript!  Go back to my example, and get one or more cells to contract.  After that, start randomly dragging a selection through the table cells.  You’ll notice that the cells sometimes snap back to the correct width, while other cells randomly contract.  You can inspect the cells with the IE8 Developer Tool to confirm that the width is indeed shifting for no apparent reason.  If that’s not a rendering bug in IE8, I don’t know what is.

“Ok, so what now?”

I’m not sure.  I’ve made a couple of posts (here and here), and I’m hoping one of them will net something useful.  If not… too bad IE.  That’s right, I’m not going to waste any more energy trying to fix this.  liteGrid still works in IE, you just occasionally get some artifacting while editing cells.  I can’t do anything about that; I’ve tried (and failed).  This is something Microsoft is going to have to fix in their buggy, standards-ignoring browser. 

What I can do though is add a new liteGrid module that will politely suggest that users install a real browser, such as Firefox or Chrome.  Look for that module Real Soon Now.

Tags:

Aug 3 2009

Integrating jEditable with liteGrid

Category: jQuery | JavaScript | liteGridMatt @ 07:37

Work continues on liteGrid (I believe that’s the name I’m going to stick with), and in fact so much has changed that I really don’t even know where to start when talking about it.  The core has been changed around a bit (for the better), many new modules have been added, an AJAX data provider has been added… it’s been a busy couple of weeks.  Today, I thought I would start with something fairly straight-forward: a new module that brings the power of jEditable into liteGrid. 

If you recall my previous post, I basically wrote all the cell-editing script from scratch.  It worked well enough and was extensible, but as I noted in my article, James Kolpack pointed out that I really did a lot more work than was necessary.  So, in what little “spare time” I have, I implemented a module that achieves the same result (click-to-edit) using jEditable instead of custom script.  This has several advantages.  First, jEditable has a fairly rich set of editors already, and there are even 3rd party add-ons for additional editor types.  Second, the goal of liteGrid isn’t editing, the goal is to provide a flexible, extensible, and lightweight grid.  Maintaining my own editing library wasn’t going to help me achieve that goal.  So, the old InlineEditingModule was thrown away, and JEditableModule has taken it’s place. 

Let’s start with the high-level summary first:

function JEditableModule() {

    //Key codes.
    var enterKey = 13;

    var base = this;

    //Registers for events.
    base.initialize = function(liteGrid, options) {
        ...
    }

    //Attaches jEditable to editable columns.
    base.columnBound = function(event, column, tdElement) {
        ...
    }

    //Callback that is run whenenever a cell has been saved.  This
    //stores the cell value in the underlying data item.
    base.saveCell = function(value, settings) {
        ...
    }

    //Callback that is run after a cell's value has been changed. 
    base.afterSave = function(value, settings) {
        ...
    }
}
JEditableModule.defaultOptions = { placeholder: "", onblur: "submit", type: "text" };

There is the usual initialize function that all liteGrid modules must define.  Next is an event handler that fires when columns are bound.  Finally, there are two helpers: one that is responsible for actually updating the underlying dataItem when a value changes, and one that is called after a cell has been updated.  There are also some default options that can be overridden by the liteGrid options (as we’ll see in a second).

The magic begins in the initialize function:

base.initialize = function(liteGrid, options) {
    base.liteGrid = liteGrid;
    base.options = options;

    //If jEditable isn't defined, we can't do anything.
    if (!$.editable) {
        console.log("Unable to initialize, can't find the jEditable plug-in.");
        return;
    }

    liteGrid.$el.bind("columnBound", base.columnBound);
}

If the jEditable plug-in isn’t available, initialization is aborted, and an error is logged to Firebug.  Otherwise, the module registers for columnBound events:

base.columnBound = function(event, column, tdElement) {

    //If the column isn't editable, or if we've already applied 
    //jEditable, don't do anything.
    if (column.editable !== true || tdElement.hasClass("editable")) {
        return;
    }

    //Additional options are stored in the settings, making them available to callback functions.
    var options = $.extend({}, JEditableModule.defaultOptions, { callback: base.afterSave, column: column, tdElement: tdElement });

    if (column.type) {
        //If the type isn't supported, alert the user.
        if (!$.editable.types[column.type]) {
            console.warn("Unable to find editor for type " + column.type + " in jEditable.");
            return;
        }

        options.type = column.type;
    }

    //Special-case: the built-in select editor requires additional properties that define the options.
    if (column.type == "select") {
        options.data = column.selectOptions;
        //This will end edit mode when the user presses enter.
        tdElement.keyup(function(event) { if (event.keyCode == enterKey) $("select", tdElement).blur(); });
    }

    //Make the element editable.
    tdElement.editable(base.saveCell, options);
    tdElement.addClass("editable");
}

If the column isn’t editable, or if it has already been processed (as indicated by the marker class “editable”), nothing is done.  Otherwise, an options object is built up that will be passed on to jEditable.  If the column type doesn’t have a defined editor in jEditable, a warning is logged, and processing terminates.  While jEditable likes to post changes to a URL via AJAX by default, it also supports a callback to handle the save, which is leveraged here with base.saveCell.

For “select” types, which render as dropdown lists, a couple of extra steps are required.  First, the options for the select are copied from the column definition.  I’m not completely satisfied with this approach as I do not like how jEditable requires you to specify your columns, but it works (for now).  Second, the standard textbox editors persist their changes when the user hits the enter key.  That doesn’t happen for dropdown lists, so a function is attached that triggers the blur event for the select, thereby triggering the value to persist. 

The saveCell function is called by jEditable when the user has indicated that they want to persist a new value:

base.saveCell = function(value, settings) {

    var cell = $(settings.tdElement);

    //See if the value actually changed
    var dataItem = cell.parent().data("dataItem");

    var currentValue = dataItem[settings.column.field];

    //An event is raised so that interested parties can modify the
    //value prior to attempting to persist it.  
    //TODO: ADD HOOKS FOR VALIDATION!
    var event = $.Event("valueChanged");
    event.currentValue = currentValue;
    event.newValue = value;
    event.column = settings.column;

    base.liteGrid.$el.trigger(event);

    //Subscribers may have modified the new value
    value = event.newValue;

    //If the value hasn't changed, or if the value is still null/empty, don't do anything.
    if (currentValue == value || ((currentValue || null) == null && value == "")) {
        settings.valueChanged = false;
    }
    else {
        //Mark the cell as having been changed.  This is used by the 
        //callback handler.
        settings.valueChanged = true;
        cell.addClass("modified");

        dataItem[settings.column.field] = value;
        cell.parent().data("dataItem", dataItem);
    }

    return value;
}

The underlying data item is retrieved from the parent row so that the new value can be compared to the current value.  An event is fired with column, new value, and current value.  This allows interested parties to modify the value if they so choose.  Ideally, validation could also be handled here, but I haven’t added that (yet).  A simple check is performed to see if the new value has changed.  If so, the cell is marked as changed, and the data item is updated.

After jEditable has saved the new value using the saveCell method, it calls the afterSave function:

base.afterSave = function(value, settings) {
    if (settings.valueChanged == true) {
        base.liteGrid.$el.trigger("columnBound", [settings.column, settings.tdElement]);
    }
}

This callback looks to see if the value actually changed, and if so, raises the columnBound event.

And that’s it.  It’s considerably simpler than the old method, and aside from a few rendering bugs with IE 8 that I haven’t ironed out yet, I don’t see any reason to use the old InlineEditModule instead of this new JEditableModule. 

Crowd: “This is all well and good, but let’s see it in action!”

Yeah, I’m working on that.  Once I get it migrated to Google Code, I’ll stand up some demos that people can play around with.  Until then, you’ll just have to believe me when I tell you that it works.

Tags:

Jul 31 2009

liteGrid: coming soon to Google Code!

Category: jQuery | JavaScriptMatt @ 00:48

My employer has approved the open-sourcing of project “liteGrid”, so that means I’ll be migrating it to Google Code in the Real Near Future.  Before I do that though, I need to come up with a final name.  I have two candidates right now: liteGrid and Webcel (like Excel, but for the web!).  I’m leaning towards Webcel.  Anyone have any other suggestions or prefer one name over the other?  If so, leave your feedback in the comments.

Tags:

Jul 29 2009

Merging and minimizing JavaScript files with YUI Compressor and PowerShell

Category: PowerShell | JavaScriptMatt @ 07:26

One of the tenants of liteGrid is that it’s modular, with the core doing as little as possible, and all the richness being layered on top by various pluggable modules.  These modules are currently spread across multiple files (one per module).  As liteGrid gets closer to “production,” it became time to merge things into a single file (no one wants to include 17 JS files).  I also wanted to minify/minimize the scripts to insure faster downloads.  The YUI Compressor is a good command-line tool for compressing JavaScript and CSS, and it turns out that it can actually be used to merge files, too.  I wrote a simple one-line PowerShell script that does all the magic.  Get ready for it… here it is:

gc *.js | java -jar yuicompressor-2.4.2.jar --type js -o liteGrid.min.js

The script assumes that “java” is in your path and that yuicompressor is in the current directory.  Enjoy!

I’ll get back to writing longer (better?) blog posts next week, this week is filled with deadlines and other not-fun stuff.

Update: Rookie mistake: you want to be sure you delete the minimized file before regenerating it, otherwise it’s old contents will be pulled in as well (not good).

Tags:

Jul 13 2009

Introducing liteGrid, a lightweight jQuery grid plug-in

Category: JavaScript | jQueryMatt @ 04:49

At my day job, we’re basically stuck re-implementing Excel in a web environment.  (Sidenote: Rob tells me that this is absolutely the correct way to build applications and all web applications should try to mimic desktop applications as much as possible.  I don’t like it, but it’s the cards we’ve been dealt, and we have to make it work.)  Right now, we’re using jqGrid with ASP.NET MVC, and we’ve got things working at a rudimentary level. However, we’ve found ourselves tripping more and more over jqGrid bugs, limitations, and just plain *pain* when trying to extend it.  My take on jqGrid is that it’s too heavy to be flexible, plus it’s not a perfect match for what we’re trying to build in this application.  It’s actually becoming a barrier to progress.  Though there are alternatives, there is nothing that we’ve found that has all the functionality we need.  So, I have now been tasked with creating a new plug-in from scratch that is flexible enough to support our needs.  I have dubbed this effort liteGrid (the name sucks because there are 10,000 table plug-ins, and all the good names have been taken).

First, here the requirements I laid out for myself as I built liteGrid.  Some of these are based on application needs, but many are just based on design principles that I felt were important:

  • Must support tree-grid functionality.  You should be able to expand and collapse rows.
  • Must support flexible editing.  Items in the grid should be editable, and it should be very clean and easy to customize how things are edited.
  • Easy to customize.  If you want to change something on the grid, it should be easy and straightforward.
  • Easy to handle formatting.  You should have complete control over how values are rendered.
  • Completely decoupled data model.  The underlying data model should be distinct from what’s rendered.
  • AJAX support.  At a minimum, loading and saving data should be doable with AJAX.
  • Simple.  The grid itself should contain very little code.
  • Event-driven.  The grid should be loosely-coupled and use events to pass messages.  I was inspired by this article on custom events in jQuery.
  • jQuery UI support.  I hate coming up with themes for anything as I have no artistic inclination (at all), so this is me passing the buck. I’m still not sure how feasible this is as I’m a complete newb on jQuery UI, but hopefully they have something I can leverage.

Before we go any further …

DISCLAIMER

I do not consider myself to be a JavaScript or jQuery guru (yet).  This is also a work in-progress. I fully expect it to undergo major changes as additional functionality is added.  I’m not claiming (yet) that it’s the best grid out there, just that I like the way it is shaping up.  Still, all feedback is welcomed at this point, so feel free to throw rocks at it.  Oh, and this may/may not eventually be released in final form.  It depends on how my employer wants to handle it.  This code is not free and open-source yet, so read it at your own risk (though I really don’t think anyone is going to care). 

Getting Started

There is a “right” way to build a jQuery plug-in.  And this is it.  I used Starter to generate my skeleton plug-in, which saved me quite a bit of time.  From there, I modeled the core of liteGrid based loosely on how ASP.NET data grids work.  The grid raises events when certain things happen, which interested parties can then listen for and respond to.  I wanted to keep the core extremely simple and light, so I also built in support for pluggable modules that hook into the grid to provide additional functionality.  The tree-grid, which I’ll show in a future post, is implemented a liteGrid module.  Also, liteGrid supports a data provider model.  By default, it uses a null provider that returns no data, but you can drop in any objection you want to serve as the provider.  By default, liteGrid requires only a single provider method, but as you will see in future posts, liteGrid modules may add additional data provider requirements.

Alright, so code time.  Let’s look at the plug-in as a whole before we dive in to the interesting methods:

(function($) {

    // Declare namespace if not already defined
    if (!$.inrad) {
        $.inrad = new Object();
    }

    //This is the actual liteGrid plug-in class.
    $.inrad.liteGrid = function(el, options) {
        // To avoid scope issues, use 'base' instead of 'this'
        // to reference this class from internal events and functions.
        var base = this;

        // Access to jQuery and DOM versions of element
        base.$el = $(el);
        base.el = el;

        // Add a reverse reference to the DOM object
        base.$el.data("inrad.liteGrid", base);

        //This actually performs the initialization.
        base.init = function() {
            ...
        }

        //Rendes the actual table.  This can be overriden by modules.
        base.render = function() {
            ...
        }

        //Renders the specified row.
        base.renderRow = function(dataItem, index) {
            ...
        }

        //Builds a row that can be inserted into the table.  columnBound
        //events are raised, and the formatter is used to format cell
        //values.
        base.buildRow = function(dataItem) {
            ...
        }

        //Inserts a row for a data item after the specified row
        //that's already in the table.
        base.insertRowAfter = function(dataItem, existingRow) {
            ...
        }

        base.init();
    }


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


    // This is the actual plug-in function.  It creates and returns
    // a new instance of the plug-in.  
    $.fn.inrad_liteGrid = function(options) {
        return this.each(function() {
            (new $.inrad.liteGrid(this, options));
        });
    }

    // This function breaks the chain, but returns
    // the inrad.liteGrid if it has been attached to the object.
    $.fn.getinrad_liteGrid = function() {
        return this.data("inrad.liteGrid");
    }

})(jQuery);

Aside from the JavaScript added by jQuery Starter, there’s not really much going on.  liteGrid is a simple object with only a few methods: init, render, renderRow, buildRow, and insertRowAfter.  I expect this list to grow a little bit as I find common functionality that feels like it belongs on the core object, but still, this is far from a complicated object. 

Let’s look at the options that are supported right now: columns, dataProvider, modules, missingValue, and rowIdColumn.  Columns is simply an array of objects that defines the columns to be rendered.  Note that modules can modify this definition to add new columns as needed, as we’ll see in the tree-grid module.  Next is the data provider.  By default, this is a null provider that just returns an empty array.  Users of liteGrid should supply their own provider or use one of the available providers that will “ship” when this thing is finished (including an AJAX provider).  Next is the array of add-on modules for the grid.  These are initialized by liteGrid and can do all sorts of crazy things.  Next we have the value to substitute for rows that are missing a value for a column.  This may be a bit of YAGNI creeping in, so I might remove it later.  Finally, we have the name of the column (we’re talking data model column) that contains the unique identifier for the row.  This should probably renamed to “rowIdProperty” for clarity.  Basically, the underlying data objects that are returned by the grid’s data provider must expose a unique identifier, and this is the name of that identifier.

Alright, time for some code.  Let’s dig in to the init method first:

base.init = function() {

    base.options = $.extend({}, $.inrad.liteGrid.defaultOptions, options);

    //Initialize all modules!  Modules might, for example, add
    //new columns (such as the expander column), add decorators to
    //the providers, subscribe to events, or do other fun things.
    $(base.options.modules).each(function() {
        console.log("Initializing %s...", this.constructor.name);
        //TODO: Add error-handling!
        this.initialize(base, base.options);

        console.log("Finished!");
    });

    base.render();
    base.$el.trigger("tableRendered");
}

There is the standard jQuery stuff for handling options.  After that, all the modules are initialized.  This is done prior to *anything* else happening because modules can do whatever they want to the grid.  They can override methods, they can change options, they can register event handlers, whatever.  Next, we render the grid, and fire a custom event on the actual table element itself that signifies that rendering is complete.  Pretty simple!

Next up is the render method:

//Rendes the actual table.  This can be overriden by modules.
base.render = function() {
    //Clear any existing table markup
    base.$el.html("");

    //Build the header.
    var headerRow = $("<tr></tr>");
    $("<thead></thead>").append(headerRow).appendTo(base.$el);

    $(base.options.columns).each(function() {
        headerRow.append("<th>" + (this.header || this.field) + "</th>");
    });

    //Grab data and add the rows.
    var dataArray = base.options.dataProvider.getData();

    $(dataArray).each(function(index) {

        base.renderRow(this, index);
    });

    //The table has been rendered, so trigger the event.
    base.$el.trigger("tableUpdated", base);
}

This method does a lot, so it defers a lot of logic to helpers where it can.  First, it wipes out any existing HTML content, and builds a header row for the table based on the column definitions.  Next, the data items are retrieved from the configured provider, and a row is rendered for each table.  Finally, an event is raised to signify that the grid has changed in some way.

Next is our helper to render a row.   I’ll also throw in a related method that builds the actual row:

//Renders the specified row.
base.renderRow = function(dataItem, index) {

    var row = base.buildRow(dataItem);

    base.$el.append(row);
}

//Builds a row that can be inserted into the table.  columnBound
//events are raised, and the formatter is used to format cell
//values.
base.buildRow = function(dataItem) {

    //Spit out values for each of the columns
    var row = $("<tr></tr>");

    //Add a class containing the ID
    row.addClass("row-id-" + dataItem[base.options.rowIdColumn]);

    //Bind the actual data item to the row so that we can get it later.
    row.data("dataItem", dataItem);

    $(base.options.columns).each(function() {
        var column = this;

        //Format the value.
        var value = (dataItem[column.field] || base.options.missingValue));

        var element = "<td>" + value + "</td>";
        element = $(element).appendTo(row);

        base.$el.trigger("columnBound", [column, element]);
    });

    //Raises the "rowBound" event on the table.
    //TODO: Can we hand the user the row's index?  Probably not...
    base.$el.trigger('rowBound', [row, dataItem]);

    return row;
}

renderRow simply calls buildRow to build up the DOM tree for the row, then appends it to the table.  buildRow is a little more complicated.  First, the new row is tagged with a special class that stores the row’s ID.  The jQuery data function is also used to store the raw dataItem with the row, making it easy to grab the raw data later.  Next, a cell is rendered for each column.  Note that the data item may define fields beyond the column specifications, it is completely flexible in this regard.  If the data item doesn’t define the column, the missing value is used instead.  When it’s all finished, an event is triggered, again on the table element, that allows interested parties to see what’s going on.

The last method our grid exposes is a helper for modules to use.  It inserts a row after an existing row in the table:

base.insertRowAfter = function(dataItem, existingRow) {

    //Create the row.
    var row = base.buildRow(dataItem);

    //Insert it.
    row.insertAfter(existingRow);

    //The table has been changed, so fire the event.
    //TODO: Do we *really* want to fire this after every insert?  Would
    //it be better to have a way to supress the event being fired, and
    //allow modules to trigger the event?
    base.$el.trigger("tableUpdated", base);

    return row;
}

It uses the buildRow function, insuring that all our events are fired, adds it to the DOM, and triggers an event indicating that the table has changed.

And that’s all there is to it!  How do we put this grid into action?  Here’s a simple example:

$(function() {
    //Subscribes to events just to make sure they work.
    $("#myTable").bind("rowBound", function() {
        //alert("Received rowBound event!");
    })
    .bind("columnBound", function(event, column, element) {
        //element.html(column.field + ": " + element.html());
    })
    .bind("tableRendered", function(event) {
        //Do some table-specific processing?
    });

    //Turn #myTable into a rich table.
    $("#myTable").inrad_liteGrid(
        {
            columns: [
                { field: "Name" },
                { field: "Value", header: "My Value" },
                { field: "Other" }
            ],
            dataProvider: new MockDataProvider(),
            modules: [new TreeGridModule(), new StripifyModule()]
        });
}
);

This tells the plug-in to convert the table with ID #myTable to a liteGrid.  We are passing in a custom data provider for testing:

function MockDataProvider() {
    this.getData = function() { 
        return [
            {ID:1, Name:"Name1", Value:"Value1", Cost:1234, HasChildren:true},
            {ID:2, Name:"Name2", Value:"Value2", Cost:12345, HasChildren:true}
        ];
    };
    
    //Gets child data from the server.
    this.getChildData = function(parentId) {
        if (parentId == 1) {
            return [
                { ID: 3, Name: "Child1", Value: "Value3", Cost: 1234, HasChildren:true },
                { ID: 4, Name: "Child2", Value: "Value4", Cost: 1234, HasChildren:false }
            ];
        }
        else if (parentId == 2) {
            return [
                { ID: 5, Name: "Child1", Value: "Value3", Cost: 1234 },
                { ID: 6, Name: "Child2", Value: "Value4", Cost: 1234 }
            ];
        }
        else if (parentId == 3) {
            return [
                { ID: 7, Name: "Child1", Value: "Value3", Cost: 1234 },
                { ID: 8, Name: "Child2", Value: "Value4", Cost: 1234 }
            ];
        }
        else { 
            return [];
        }
    }
}

Note that this provider actually has a few things that are required by the tree-grid module, which I’ll show in the next post, but liteGrid doesn’t care.  It simply looks for matches between your column definitions and the fields on your data items, and skips anything that doesn’t match.

There you have it, liteGrid.  Before I end this post, let’s look at a simple module that stripes the rows in the table:

function StripifyModule() {
    var base = this;
    
    //Registers for events that signal that the table needs to be 
    //reipified.
    base.initialize = function(liteGrid, options) {

        //Register for the events we care about.
        liteGrid.$el.bind("tableUpdated", function(event, table) {

            //Remove even/odd classes from everything
            table.$el.find("tbody tr").removeClass("even odd");

            table.$el.find("tbody tr:even").addClass("even");
            table.$el.find("tbody tr:odd").addClass("odd");
        });
    }
}

By simply subscribing to events, this module can now apply stripes to the table anytime a row is added or removed.  This is a very simple example, and the true power of this functionality won’t really be obvious until the next post, when I show the TreeGridModule.

Alright, so, comment away.  What do you think?  Any suggestions for improving things?

Tags:

May 13 2009

ASP.NET MVC HtmlHelper for Uploadify, Take One

Category: ASP.NET | MVC | JavaScriptMatt @ 06:56

As I’ve mentioned before, I really, really hate the way most people seem to be creating reusable UI “controls” with ASP.NET MVC.  I do not like emitting JavaScript, HTML, etc. from within C# code.  It’s cumbersome to create, difficult to really test, and just a real PITA in general.

Based on feedback I received from Rob after my attempts at creating a helper for jqGrid, I decided to take a completely different approach when it was time to wrap another jQuery plug-in: Uploadify.  My goal was to minimize the amount of tag-soup embedded in my C# code while still maintaining the ease-of-use of the jqGrid helper, which required only a single HtmlHelper call to go from nothing to full grid.

Well, one painful afternoon later, I think I’ve arrived at something that makes some sense.  First, I couldn’t completely eliminate the tag soup, but I did minimize it (I think) while still keeping the thing extremely simple to use and (hopefully) maintain.  Let’s start with how you would use it:

<asp:Content ContentPlaceHolderID="HeadContent" runat="server">
    <%=Html.Uploadify("fileInput", 
        new UploadifyOptions
           {
               UploadUrl = Html.BuildUrlFromExpression<SandboxController>(c => c.HandleUpload(null)),
            FileExtensions = "*.xls;*.xlsx",
            FileDescription = "Excel Files",
            AuthenticationToken = Request.Cookies[FormsAuthentication.FormsCookieName] == null ?
                string.Empty :
                Request.Cookies[FormsAuthentication.FormsCookieName].Value,
            ErrorFunction = "onError",
            CompleteFunction = "onComplete"
           }) %>
           
    <script type="text/javascript">
        function onError() {
            alert('Something went wrong.');
        }
        function onComplete() {
            alert('File saved!');
        }
    </script>                                                   
</asp:Content>

The first parameter is the name of the input control to convert to an uploadify control, the second contains all the optional settings you can customize.  I prefer to use an options class like this rather than provide 50,000 overloads.  By using a dedicated options class, I can add new settings without breaking existing code or having to create new overloads.  The options should be fairly self explanatory, but here they are:

/// <summary>
/// Defines all options for <see cref="HtmlHelperExtensions.Uploadify"/>.
/// </summary>
public class UploadifyOptions
{
    #region Public Properties

    /// <summary>
    /// The URL to the action that will process uploaded files.
    /// </summary>
    public string UploadUrl { get; set; }

    /// <summary>
    /// The file extensions to accept.
    /// </summary>
    public string FileExtensions { get; set; }

    /// <summary>
    /// Description corresponding to <see cref="FileExtensions"/>.
    /// </summary>
    public string FileDescription { get; set; }

    /// <summary>
    /// The ASP.NET forms authentication token.
    /// </summary>
    /// <example>
    /// You can get this in a view using:
    /// <code>
    /// Request.Cookies[FormsAuthentication.FormsCookieName].Value
    /// </code>
    /// You should check for the existence of the cookie before accessing
    /// its value.
    /// </example>
    public string AuthenticationToken { get; set; }

    /// <summary>
    /// The name of a JavaScript function to call if an error occurs
    /// during the upload.
    /// </summary>
    public string ErrorFunction { get; set; }

    /// <summary>
    /// The name of a JavaScript function to call when an upload
    /// completes successfully. 
    /// </summary>
    public string CompleteFunction { get; set; }

    #endregion
}

Next, we have the actual HtmlHelper extension method:

/// <summary>
/// Renders JavaScript to turn the specified file input control into an 
/// Uploadify upload control.
/// </summary>
/// <param name="helper"></param>
/// <param name="name"></param>
/// <param name="options"></param>
/// <returns></returns>
public static string Uploadify(this HtmlHelper helper, string name, UploadifyOptions options)
{
    string scriptPath = helper.ResolveUrl("~/Content/jqueryPlugins/uploadify/");

    StringBuilder sb = new StringBuilder();
    //Include the JS file.
    sb.Append(helper.ScriptInclude("~/Content/jqueryPlugins/uploadify/jquery.uploadify.js"));
    sb.Append(helper.ScriptInclude("~/Content/jqueryPlugins/uploadify/jquery.uploadify.init.js"));

    //Dump the script to initialze Uploadify
    sb.AppendLine("<script type=\"text/javascript\">");
    sb.AppendLine("$(document).ready(function() {");
    sb.AppendFormat("initUploadify($('#{0}'),'{1}','{2}','{3}','{4}','{5}',{6},{7});", name, options.UploadUrl,
                    scriptPath, options.FileExtensions, options.FileDescription, options.AuthenticationToken,
                    options.ErrorFunction ?? "null", options.CompleteFunction ?? "null");
    sb.AppendLine();
    sb.AppendLine("});");
    sb.AppendLine("</script");

    return sb.ToString();
}

The helper uses a StringBuilder (yeah, I hate them, and I’m open to suggestions) to include two JavaScript files.  The first is the standard uploadify script, but the second is something custom, which I’ll get to in just a second.    Finally, the helper outputs a call to initUploadify inside of the page load event, passing in all the options that were specified.

And that brings us to that second JavaScript include:

//This is used in conjunction with the HtmlHelper.Uploadify extension method.
function initUploadify(control, uploadUrl, baseUrl, fileExtensions, fileDescription, authenticationToken, errorFunction, completeFunction) {
    var options = {};

    options.script = uploadUrl;
    options.uploader = baseUrl + 'uploader.swf';
    options.cancelImg = baseUrl + 'cancel.png';
    //TODO: Make this an option?
    options.auto = true;
    options.scriptData = { AuthenticationToken: authenticationToken };
    options.fileExt = fileExtensions;
    options.fileDesc = fileDescription;

    if (errorFunction != null) {
        options.onError = errorFunction;
    }

    if (completeFunction != null) {
        options.onComplete = completeFunction;
    }

    control.fileUpload(options);
}

In here, I’ve created a simple JavaScript function that actually calls the uploadify JavaScript plug-in.  By using this method instead of using C# to emit the configuration code directly, I’m cutting out a fair amount of tag soup, and I’m wrapping things up in a way that will be easier to change in the future.  Hopefully.  The down side to this approach is that you have to create a new JavaScript method and include for every plug-in you want to use, but combining the scripts and correctly setting cache headers should reduce the request overhead.

I’m not claiming that this is the best way to do this.  In fact, I really hope it isn’t, because I still don’t like it.  But I think that I like it better than the approach I took for jqGrid.  If you have any suggestions or feedback, please share.  Feel free to tell me that I’m doing things completely wrong.

Tags:

May 6 2009

Prevent users from abandoning changes accidentally with jqGrid

Category: JavaScriptMatt @ 05:22

In the project I’m working on, we use jqGrid to display hierarchical records in an Excel-like manner.  Changes are queued up on the client and submitted to the server whenever the user clicks the “Save” button.   That means a user could make a bunch of changes, then navigate away from the page accidentally, and lose everything they just did.  Preventing that is easy enough though, just use this JavaScript snippet:

   1: window.onbeforeunload=checkForChanges;
   2: function checkForChanges(){
   3:     var rows = $('#treeTable').getChangedCells('all');
   4:     if (rows != null && rows.length > 0) {
   5:         return 'You have unsaved changes that will be discarded.';
   6:     }
   7: };

The checkForChanges function is bound to the onbeforeunload event, which is fired whenever the browser window is about to change the page.  This includes page reload/refreshes or anything like that.  The checkForChanges function uses the jqGrid getChangedCells method to see if there are any unsaved changes.  If there are, it returns a message that will be shown automagically in a JavaScript confirm dialog.  If there aren’t, it doesn’t return anything, and the user is allowed to navigate without being nagged.

Tags:

Nov 18 2008

A long time ago, in a GridTreeView far, far away&hellip;

Category: ASP.NET | JavaScriptMatt @ 15:37

Some of you (and by some, I mean two) have been axiously awaiting the release of the code for the GridTreeView that I described a while back. Thanks to the generosity of my current employer, I’m happy to present the code, a compiled DLL, and a demo ASP.NET MVC site that you can use to test out the GridTreeView.  Enjoy!

Binaries

Source Code

Tags:

Oct 8 2008

Creating a reusable grid tree view with ASP.NET MVC and jQuery

Category: ASP.NET | JavaScript | MVCMatt @ 08:45

I think it is a safe assumption that every web developer has had to display tabular data at one point or another.  Tabular data is easy with ASP.NET: bind a GridView to a data source, and you're all set.  But with ASP.NET MVC, things are a little trickier.  We don't have access to all the nice WebForms controls.  Still, it's fairly easy to do: just write a for-loop, or better yet, use the grid helper from MvcContrib.

Things get a trickier though if your tabular data is also hierarchical.  Typically, we display hierarchical data in a tree of some kind, but trees really aren't great for tabular data.  What would be great is to combine the two somehow.  Fortunately, there's a nice plug-in for jQuery that does just that: ActsAsTreeTable.  It's easy enough to use; all you have to do is embed ID's and CSS class information in your table rows, and the JavaScript does everything else.  Here's a simple example from the docs:

   1: <link href="path/to/jquery.acts_as_tree_table.css" rel="stylesheet" type="text/css" />
   2: <script type="text/javascript" src="path/to/src/jquery.acts_as_tree_table.js"></script>
   1:  
   2: <script type="text/javascript">
   3:     
   4: $(document).ready(function()  {
   5:     $("#your_table_id").acts_as_tree_table();
   6: });
</script>
   3:  
   4: ...
   5:  
   6: <table id="tree">
   7:   <tr id="node-1">
   8:     <td>Parent</td>
   9:   </tr>
  10:   <tr id="node-2" class="child-of-node-1">
  11:     <td>Child</td>
  12:   </tr>
  13: </table>

We can now combine this with the grid from MvcContrib to produce a collapsable Grid Tree View.  This example is encapsulated inside a view user control so that it can be used on any page.  It displays imaginary "widgets" in a tree.  The widgets aren't really hierarchical, so I've fudged it by making it appear that each widget is a child of its predecessor in the table.

First, let's create the grid:

   1: <%
   1:  Html.Grid(GetWidgets(), new Hash(id => ClientID, style => "width:100%"), 
   2: column =>
   3:       {
   4:           column.For(w => w.Name);
   5:           column.For(w => w.Description);
   6:           column.For(w => Html.TextBox("Description_" + w.Id, c.Description), "Editable").DoNotEncode();
   7:       },
   8: sections =>
   9:     {
  10:         sections.RowStart(c =>
  11:                               {
%> <tr class="child-of-node-<%=w.Id - 1%>" id="node-<%=w.Id%>"> <%
   1:  
   2:                               });
   3:     }    ); 
%>

That probably looks horrendous, so let's walk through it.  GetWidgets() is a method on the view user control that grabs widgets from wherever (in practice, probably the model or view data).  Next, the Hash just contains key/value pairs that are embedded in the opening table tag; here, we've specified the table's ID (by using ClientID, it will have the name that ASP.NET gives to the user control), and we've specified that it should be 100% wide.  Next, we define the columns using lambda expressions.  The first two columns simply display the widget's name and description.  The last column is a little more complicated.  It creates a text box using the TextBox helper method.  Since the column contains HTML that shouldn't be encoded, we call DoNotEncode on it.  Finally, we use a lambda expression to override how rows are created.  The code here populates the row with the 'child-of-node-#' class and the id attribute, both of which are needed by ActsAsTreeTable.  It may look intimidating, but it's actually nice once get comfortable with the syntax. 

The last thing we need to do is spit out the JavaScript to turn our gird into an ActsAsTreeTable:

   1: <script type="text/javascript">
   2:         $(document).ready(function()  {
   3:             $("#<%=ClientID %>").acts_as_tree_table();
   4:         });
   5: </script>

If you set everything up correctly, you should now have a working "grid tree view".  In a future article, I'll introduce a new Html helper that does all the heavy lifting for you.

Tags: