Dec 3 2009

Easy vertical and bottom tab panels with jQuery UI

Category: jQueryMatt @ 09:29

jQuery UI includes a tabs widget that makes it easy to add tab panels to your web applications.  Out-of-the-box, it currently only supports horizontal tab strips across the top of the content, like so:

normalTabs

There are samples in the jQuery UI code that show how you can easily achieve either side (vertical) tabs or bottom (horizontal) layouts with some simple jQuery and CSS, but I didn’t want to have to copy-and-paste the code every time I needed non-standard tab layouts.  I’ve packaged the styles and script up into the superTabs library which introduces two new jQuery plug-in methods:

$("#verticalTabs").verticalTabs();

$("#bottomTabs").bottomTabs();

These take the standard jQuery UI tabs options.  Here’s the tabs widget with vertical tabs:

verticalTabs

And here are bottom tabs:

bottomTabs

You can see a demo in action or download the code.  Enjoy!

Tags:

Oct 29 2009

An auto-hiding menu bar with jQuery

Category: jQuery | UIMatt @ 04:52

What do you do with links/buttons/actions that are likely to be infrequently used, but should still be available to users anywhere in the system?  That’s the question I had to answer for the new InSpire search engine we’re working on at my day job.  The sort of links I am talking about are logout, preferences, help, and various admin commands (for administrators only).  These actions are not really part of the application per se, they are more part of the basic application framework that needs to be available to the user.  We didn’t want to take up valuable space in our UI with these things (plus we wanted to keep the UI as basic as possible), so where do you put these things?

I decided I would try an auto-hiding sliding menu.  The behavior I wanted was identical to the Windows task bar when the Auto-hide the task bar option is selected.  After a little jQuery and CSS magic, and some help from Jeff Robbin’s great article on an auto-hiding search box, I had a solution that works in IE 8, Firefox, and Chrome.

First, check out the demo.  The menu bar is themed using jQuery UI, so if your app uses jQuery UI, you will get a consistent look-and-feel.  Also note that my menu bar doesn’t suffer from the same bug that Jeff Robbin’s article does: when you mouse-over and mouse-out my menu bar very quickly a few times, it doesn’t get stuck in a show/hide loop.  Let’s look at the code to see why.  First, the markup and CSS:

<style type="text/css">
#siteMenuBar
{
    position: fixed;
    top: 0px;
    left: 0px;
    width: 100%;
    height: 30px;
    margin-top: -27px;
}

#siteMenuBar > #menuContainer
{
    margin-left: auto;
    margin-right: auto;
    width: 100%;
    height: 25px;
    padding-top: 5px;
}

#siteMenuBar ul
{
    float: right;
    list-style-type: none;
    margin: 0;
    padding: 0;
    font-size: 0.9em;
}

#siteMenuBar li
{
    display: inline-block;
    margin: 0;
    padding: 0 5px 0 5px;
    margin-right: 10px;
}

#siteMenuBar ul:first-child
{
    float: left;
    margin-left: 10px;
}
</style>
...
<div id="siteMenuBar" class="ui-widget">
    <div id="menuContainer" class="ui-widget-header">
        <ul>
            <li><a href="#">Administration</a></li>
            <li><a href="#">Design Guidelines</a></li>
            <li><a href="#">Other Link</a></li>
        </ul>
        <ul>
            <li><a href="#">Preferences?</a></li>
            <li><a href="#">Logout</a></li>
        </ul>
    </div>
</div>

The markup is pretty clean.  There is an outer container for the menu, then an inner container for the actual menu.  The inner container is probably not necessary, but having it allows you the opportunity to do some neat style nesting.  The actual menu is implemented as two unordered lists.  The CSS floats one of the menus to the left, and the other to the right, giving you two groups to add menu items to. 

The styles are a little more complex.  The outer container is giving a fixed position at the top of the window, then positioned mostly off screen with negative margin.  This leaves a small handle visible that the user can mouse-over to show the menu.  The inner container applies a little padding to the menus and could be used to make the menu bar appear to be less than 100% of the window width (though in this case it is allowed to fully fill the menu bar area).  Next, the actual menus are styled and positioned.  Note that both are initially floated to the right, but using the pseudo-selector first-child, we bump the first menu back to the left. 

Now let’s look at the code:

var timeout = null;
var initialMargin = parseInt($("#siteMenuBar").css("margin-top"));

$("#siteMenuBar").hover(
    function() {
        if (timeout) {
            clearTimeout(timeout);
            timeout = null;
        }
        $(this).animate({ marginTop: 0 }, 'fast');
    },
    function() {
        var menuBar = $(this);
        timeout = setTimeout(function() {
            timeout = null;
            menuBar.animate({ marginTop: initialMargin }, 'slow');
        }, 1000);
    }
);

Note the timeout variable.  This is key in fixing the bug that exists on auto-hiding search box.  For flexibility, we also grab the top margin so that we can hide the menu back to its original state.

The real core of the technique is the jQuery hover handler.  We supply two functions: one to execute when the mouse hovers over the menu bar, the other to handle mouse out.  The first time the mouse over handler is executed, the timeout will be null, so it will immediately animate the menu bar, removing the negative margin and making it visible.  On mouse out, the setTimeout function is used to schedule the menu to be hidden one second later.  Note that I’m assigning the return of setTimeout to my timeout variable.  This is what prevents the menu bar from ending up in a bounce-loop like the search box.  It allows me to cancel the hide action.  If the bar is scheduled to be hidden and the user moves the mouse back over the menu bar, the mouse over handler clears the timeout.  Without that bit of logic, even if you move mouse back over the menu bar, the setTimeout function will hide the menu out from under the user, which can lead to that funky show/hide cycle. 

So, there you have it: a simple, clean auto-hide menu with jQuery.  This could easily be encapsulated in a jQuery plug-in for easy re-use (not really needed in my case though since the master page is responsible for rendering it).  Anyway, let me know if you have any suggestions!

Tags:

Oct 14 2009

Improving jQuery&rsquo;s Validation Plug-in

Category: jQueryMatt @ 08:24

One of the 50 or so tasks I’m juggling at my day job deals with coming up with UI standards for one of our applications.  I’m trying to think through every common scenario we have, design how it should work from a UI stand point (both the perceived UI as well as the HTML markup and CSS), and create examples to illustrate the standards.  One of the scenarios I thought of was forms and validation.  Ugh.  I have spent the better part of a day now fighting with the jQuery Validation plug-in.  It is the defacto standard for validation in jQuery.  While it is powerful, and I really like being able to specify validation rules by simply applying CSS classes, it leaves a lot to be desired in the area of flexibility. 

What I wanted

Here’s what I wanted for our forms and validation: fluid width, labels for each element, and (hidden) labels for element validation messages when needed.  If I didn’t want to display a validation message, I just wanted to be able to omit the label.  I didn’t want to have to do a bunch of JavaScript config.  I also wanted exact control over how the error labels were rendered.  Sometimes I wanted error labels to have a special class or other styles.  Basically, I wanted to be able to write zero JavaScript code to get a nicely-formatted, validated form that I had complete control over. 

Out of the box, while it does allow you to use your own error labels, the Validation plug-in is very dumb in how it uses them.  First, it strips all CSS classes from the labels when it unhides them.  Very, very annoying.  There does not seem to be a good way to override this behavior without resorting to major hacking (like I did below).  Second, if you do specify a custom label, it will only use it for the first validator applied to an element.  If you have multiple validators applied to a single element, it will still add a new label element for the second validator.  I ran into this as soon as I tried to have a required E-mail field.  Ugh.

The good news is that it is possible to get the simple layout I wanted with minimal JavaScript config.  The bad news is that it was an incredible pain in the rear to get there and involved a lot of browsing through the Validation plug-in’s source code. 

How I did it

One of the nice things about jQuery Validation is that you can specify defaults that will apply to all instances.  So once I figured out how to get the behavior I wanted, it was easy to encapsulate it where the entire application would be able to leverage it easily.  So what’s the solution?  Fugliness.

First, you need to specify a showErrors callback.  When specified, this is invoked and allows you to fully control the display of error messages.  It receives two parameters, an errorMap which is useless, and an errorList that contains validation errors.  My approach was to loop through the errorList, get the name of each element, look for a corresponding label element with class error, and unhide it.  This allowed me to fully control validation messages via markup, and it made it easy to choose not to display a validation message at all.  Yes, this does mean that I lost the ability to display a different error message for different validators (such as one when an E-mail field is blank versus when it has an invalid address), but I prefer to have a single validation message for each element anyway. 

Unhiding the error label is only part of the solution.  You also need to highlight the element that the error corresponds to.  This can be achieved by using the optional highlight callback.  In this callback, you can add the error class.  Be warned, you must manually call your own callback from your showErrors callback, otherwise highlight and the corresponding unhighlight callbacks will never be invoked.  This is the part that is fugly.

Here’s the full source code, which I wrapped in a .js file and included alongside the main jQuery Validation .js file.  This insures consistent behavior everywhere we use the jQuery Validation plug-in in our project.

$.validator.setDefaults({
    showErrors: function(errorMap, errorList) {
        var validation = this;
        $(errorList).each(function() {
            var error = this;
            var errorLabel = $("label[for='" + error.element.name + "'].error");
            errorLabel.css("display", "inline-block");
            validation.settings.highlight(error.element);
        })

        for (var i = 0, elements = this.validElements(); elements[i]; i++) {
            validation.settings.unhighlight(elements[i], validation.settings.errorClass, validation.settings.validClass);
        }
    },
    highlight: function(element) {
        $(element).addClass("error");
    },
    unhighlight: function(element) {
        $(element).removeClass("error");
        $("label[for='" + element.name + "'].error").hide();
    }
});

This is the final product:

validation

And the corresponding markup:

<fieldset>
    <legend>A simple comment form with submit validation and default messages</legend>
    <div class="wrapper">
        <p>
            <label for="name">
                Name:</label>
            <input type="text" id="name" name="name" size="25" class="required" minlength="2" />
            <label for="name" class="error">
                Please enter a valid name.</label>
        </p>
        <p>
            <label for="email">
                E-Mail:</label>
            <input type="text" id="email" name="email" size="25" class="required email" />
            <label for="email" class="error">
                Please enter a valid E-mail address.</label>
        </p>
        <p>
            <label for="url">
                URL:</label>
            <input type="text" id="url" name="url" size="25" class="url" value="" />
            <label for="url" class="error">
                Please enter a valid URL.</label>
        </p>
        <p>
            <label for="state">
                State:</label>
            <select name="state" id="state" class="required">
                <option></option>
                <option>Tennessee</option>
                <option>Georgia</option>
                <option>South Carolina</option>
            </select>
            <label for="state" class="error">
                Please select a state.</label>
        </p>
        <p>
            <label for="terms">&nbsp;</label>
            <input type="checkbox" name="terms" value="terms" class="required" />
            <label for="terms" class="checkbox">I have read and accept the terms.</label>
            <label for="terms" class="error">You must accept the terms.</label>
        </p>
        <p>
            <label for="comment" class="textarea">
                Your comment:</label><br />
            <textarea id="comment" name="comment" cols="22" class="required"></textarea>
        </p>
        <p>
            <input class="submit" type="submit" value="Submit" />
        </p>
    </div>
</fieldset>

It’s not perfect, but I’m happy with the markup, and I’m happy with the UI.  Hopefully I didn’t miss something obvious with the Validation plug-in that would have saved me half a days work.  If I did, feel free to tell me how stupid I am. :)

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:

Aug 21 2009

liteGrid&rsquo;s new provider model for layout

Category: jQuery | liteGridMatt @ 08:22

I just committed a major change to liteGrid.  Prior to this change, liteGrid’s core was responsible for most of the table rendering.  It would render a basic table with a header and no rich-UI functionality.  All the niceties (resizable columns, jQuery UI integration, fixed header row) were added by the LayoutManager module.  This worked, but required a few ugly hacks, such as shifting the table behind the fixed header row.  It also introduced some module-to-module coupling issues that I didn’t like.  Some modules (such as the sortable columns one I’m working on) really need to know about the rich UI added by the LayoutManager module.  I solved part of this by migrating some things, like the jQuery UI placeholders, into liteGrid’s core, but that never felt like the right solution to me.

Changing gears for a second, I always envisioned basically three ways to extend liteGrid.  The first is obvious: the add-on modules.  The second is providers that could be plugged in.  The third is by replacing built-in methods with your own.

Providers are different from modules in that they are *required* for liteGrid to function, they have a fixed spec that they must implement, and modules can reference them because they are guaranteed to be there.  Originally I had envisioned a format provider in addition to the data provider that exists today, but I ended up scraping it.  So prior to today, there was only a single provider slot (the data provider). 

Back to today’s changes: liteGrid now has a second provider that handles the layout of the grid.  That’s right, liteGrid core now contains now rendering logic.  It stubs out the rendering methods with exceptions (so that developers will get a more useful error message if they fail to implement a layout provider correctly), but it defers all the rendering to the configured layout provider.  The default provider replaces the LayoutManager add-on and includes resizable columns, a fixed header row, etc.  Since it has complete control over the markup that’s rendered, it also removes a few ugly IE-specific hacks, and makes it easier to add sorting to liteGrid.

Another cool thing about switching to a provider model is that you can now fully control how liteGrid is rendered (though there might still be a few modules that need to be updated to make this statement completely true).  Want to render it as a bunch of nested <div> elements?  Go right ahead.  Want to spit it out as an unordered list?  That’s doable, too.

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 20 2009

liteGrid &ndash; Recent Changes

Category: jQueryMatt @ 05:18

I’m very busy today, so I’ve decided to punt on the post for today and talk just briefly about some of the changes that I’ve made recently to liteGrid and why I made them.  I’m not going to talk about the layout manager module until Wednesday.  For background, be sure to read the first three parts: 1, 2, 3.

Changes to core

I did make a couple of minor changes to core. First, the aptly named “initializationComplete” event is now fired after all modules have been initialized.  I had actually planned on firing this event originally, but I decided to wait until I had a real need (YAGNI).  Now, modules are allowed to make changes to the liteGrid options (including column definitions) within their initialization methods.  If they have logic that needs to be executed once the column definitions are solidified, they should register to receive the initializationComplete event.

Next, the table header cells (th elements) are now rendered slightly differently.  First, they are given an ID of “{FieldName}Header” (I’m going to make this format an overridable option eventually).  This allows styles to target specific columns.  Next, the label within each header cell is now nested within a simple div tag.  This makes it possible to do some advanced styling (separate styles on the th and label) as well as supports the layout manager.  Yes, I basically made a change to core to support a module.  Sorry, but it was unavoidable, and it is in-line with how other grid plug-ins render their markup, so I don’t feel like it was completely unjustified.

jQuery UI Support

A major change I recently made was add jQuery UI support.  This is mainly leveraged by the layout manager module, but I also updated the tree-grid module to take advantage of it.  The expander icons are now rendered using the .ui-icon-plusthick and .ui-icon-minusthick CSS classes, and non-expandable items are rendered using .ui-icon-bullet.  It isn’t a major change, but it does reduce the number of liteGrid-specific files and insures that the grid looks pretty when used with jQuery UI themes.

What’s Left

From my first post, here’s the list of things I said I’d like to achieve with liteGrid (items marked through have been implemented):

  • 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.

Some of those are debatable, I suppose.  I certainly think liteGrid’s customization story beats the hell out of other grids, and I think the loosely-coupled design of the modules makes it much easier to digest. 

In addition to those things, I’ve also added a layout manager, complete with resizable columns, and I’m working on toolbar support.  I also plan to write a simple AJAX module that will allow you to move data to/from the server.    I’m hoping to wrap the initial version of liteGrid up this week, at which time I will see about open-sourcing it on Google Code. 

That’s it for this post.  As always, feedback is very much appreciated.

Tags:

Jul 17 2009

Editable cells with liteGrid

Category: jQueryMatt @ 05:31

Continuing my series on the development of liteGrid (part 1, part 2), this post will look at how I implemented an add-on module that provides click-to-edit functionality.  There are several different ways that you can handle editable tables in a browser, but the two most common are cell-based editing and row-based editing.  In row-based editing, an entire row is toggled into edit mode, the user makes changes to the entire row, and then clicks a save button to persist changes to the row.  Alternatively, cell-editing usually allows a user to click on a specific cell to change.  The overall aim for liteGrid is to provide a very Excel-like experience, but inside a browser, so I implemented a module that provide cell-editing functionality.  Again, I am told by Rob that this is absolutely the correct approach to go (<---sarcasm). 

Usage

When an editable cell is clicked, the cell toggles to a configurable editor.  If the user presses enter, the new value is saved to the underlying row’s data item.  If the user presses escape, the edit is canceled, and the row is restored to its original state.  Let’s look at how we would use this module:

$("#myTable").inrad_liteGrid(
    {
        columns: [
            { field: "Name", editable: true },
            { field: "Value", header: "My Value", editable: true,
              type: "dropdown", dropdownOptions: ["one", "two"] },
            { field: "Cost", editable: true, type: "currency" },
            { field: "Other", editable: true, defaultValue: "text2", 
              type: "dropdown", dropdownOptions: [
                    { text: "text1", value: 1 }, 
                    { text: "text2", value: 2 }, 
                    { text: "text3", value: 3}] 
            }
        ],
        dataProvider: new MockDataProvider(),
        modules: [new TreeGridModule(), new StripifyModule(), new InlineEditModule()]
    });

The column model has been extended slightly as InlineEditModule supports additional column properties that allow the caller to control what is editable and how its edited.  By default, columns are not editable.  The “editable: true” property must be specified to enable editing.  By default, a text editor is used, but other types are supported out of the box (with more to come later): dropdown and currency.  For the dropdown type, another column property is supported/required: the list of options.  These can be specified as either a simple string array or as an array of objects with text and value properties.  Again, all we have to do to take advantage of this module’s functionality is add a new instance of it to the liteGrid’s modules array.

The Code

</boringStuff>.  Time for some code.  Be warned, this is the most complex module for liteGrid yet!

function InlineEditModule() {

    //Key codes.
    var escapeKey = 27;
    var enterKey = 13;

    var base = this;

    //We need to know when a row has been bound, that way we can add
    //events to make it editable.
    base.initialize = function(liteGrid, options) {
        ...
    }

    //Callback that takes care of actually making the element editable.
    base.columnBound = function(event, column, tdElement) {
        ...
    }

    //Checks to see if editing is allowed, and if so, initializes everything
    //so that the cell can be edited.
    base.beginEditing = function(column, tdElement) {
        ...
    };

    //Does any post processing.  For now, it just marks the cell as being in edit mode.
    base.beganEditing = function(column, tdElement) {
        ...
    }

    //Gets an appropriate editor.
    base.getEditor = function(column) {
        ...
    }

    //This is the function that supports editing of text elements.  It displays a
    //simple text input element.
    base.editText = function(column, tdElement, dataItem) {
        ...
    }

    //An editor for currency values.
    base.editCurrency = function(column, tdElement, dataItem) {
        ...
    }

    //An editor that allows selecting a value from a drop-down list.
    //This editor requires an additional property to be specified on the 
    //column. 'dropdownOptions' should be an array of values to display 
    //in the list.
    base.editDropdown = function(column, tdElement, dataItem) {
        ...
    }
    
    //TODO: Additional editors go here!
}

First, a few “const” fields are declared to make the code more readable.  These are simply the corresponding keycodes which will be used to save/cancel an edit later. After that, we have the standard module stuff: grabbing a self-reference to eliminate scope issues and initialization which will tie the module into to liteGrid events that it cares about, the main one being the “columnBound” event.  There are two functions that help with making a cell editable, beginEditing and beganEditing.  As you will see, there is a reason this functionality is split into two methods.  Finally, we have the helper function that retrieves the appropriate editor for a cell and the various built-in editors.  All editors are implemented as functions on the module.  The nice thing about this approach is that it’s extensible.  If you want to define a custom type and editor, all you need to do is add a function to an instance of the module, then specify the type on your column model:

var inlineEditor = new InlineEditModule();
inlineEditor.editMyCustomType = function() {
    //TODO: Implement your editor!
}

$("#myTable").inrad_liteGrid(
    {
        columns: [
            { field: "Name", editable: true, type: "myCustomType" },
            ...
        ],
        dataProvider: new MockDataProvider(),
        modules: [new TreeGridModule(), new StripifyModule(), inlineEditor]
    });

I’m not going to get in to the initialize method since all it does is grab a reference to the liteGrid and register to receive columnBound events.  The real magic starts when a new column is bound:

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

    //If the field isn't editable, do nothing
    if (!(column.editable === true)) {
        return;
    }

    //If the column has already been processed, do nothing.
    if (tdElement.hasClass("editable")) {
        return;
    }

    //Determine which editor to use.
    var editor = base.getEditor(column);

    tdElement.addClass("editable")
            .click(function() {
                //Initialize editing.
                if (base.beginEditing(column, tdElement)) {

                    //This is the raw data item.
                    var dataItem = tdElement.parent().data("dataItem");

                    //Create the editor.  We have to new it since there could be 
                    //multiple fields in edit at once.
                    var newEditor = new editor(column, tdElement, dataItem);

                    //Store the editor for later.
                    tdElement.data("editor", newEditor);

                    //Do anything after that?
                    base.beganEditing(column, tdElement);
                }
            });
}

First, the function simply checks to see if the column is editable.  If not, there’s nothing to do.  It also checks for the marker class “editable” which indicates that a column has already been processed and is editable.  This is needed since a column could be rebound for a variety of reasons, and we don’t want to rebind redundant event handlers.  Next, a helper method is used to grab the correct editor for the column.  An inline function is then bound to the element’s click event.  It first checks to see if the column can be edited at this time (more on that later), and if so, toggles the editor on for the cell.  A reference to the editor is stored using jQuery’s data function so that it can be retrieved easily later.

Here’s where the real fun begins.  beginEdit is responsible for checking to see if the element is editable at this time, and it also prepares everything for the editor.

base.beginEditing = function(column, tdElement) {

    //If the field is already in edit mode, don't do anything.
    if (tdElement.hasClass("editing")) {
        return false;
    }

    //Subscribers can request that the edit be cancelled.
    var event = $.Event("cellBeginEditing");
    event.cancelEdit = false;
    //base.liteGrid.$el.trigger("cellBeginEditing", [column, tdElement]);
    base.liteGrid.$el.trigger(event, [column, tdElement]);

    if (event.cancelEdit === true) {
        return false;
    }

    //Store the current contents of the cell.
    tdElement.data("oldValue", tdElement.html());

    //This is the event handler that is responsible for
    //ending edit mode.  It's declared as a variable 
    //so that it can unregister itself after its finished.
    var onKeyUp = function(event) {
        //Escape == cancel edit, Enter == save changes
        if (event.keyCode == escapeKey ||
                event.keyCode == enterKey) {

            //Grab the editor out of the element.
            var editor = tdElement.data("editor");

            if (!editor) {
                console.error("Unable to retrieve editor reference.");
                return;
            }

            var modified = false;

            if (event.keyCode == escapeKey) {

                //Editors can define a custom method to be called
                //when editing mode is canceled.
                if (editor.cancelEdit) {
                    editor.cancelEdit();
                }

                //Restore the old contents.
                tdElement.html(tdElement.data("oldValue"));
            }
            else {

                var value = editor.getValue();

                var dataItem = tdElement.parent().data("dataItem");
                dataItem[column.field] = value;
                tdElement.parent().data("dataItem", dataItem);

                tdElement.html(value);

                //See if any changes were made.
                if (tdElement.html() != tdElement.data("oldValue")) {
                    tdElement.addClass("modified");
                    modified = true;
                }
            }

            //Clear data that we don't need, and mark 
            //the cell as being editable again.
            tdElement.removeData("editor")
                    .removeData("oldValue")
                    .removeClass("editing");

            //If changes were made, raise the column-bound event so that any 
            //other modules can re-examine the column.
            if (modified) {
                base.liteGrid.$el.trigger("columnBound", [column, tdElement]);
            }

            //Unregister the event.
            tdElement.unbind("keyup", onKeyUp);
        }
    };

    //We watch for escape and enter events.
    tdElement.bind("keyup", onKeyUp);

    return true;
}

If the element is already being edited (as indicated by the marker “editing” class), nothing happens.  Otherwise, we use jQuery’s custom event support to raise a “cellBeginEditing” event.  Subscribers to the event can choose to block the edit by setting “cancelEdit” to true.  If none of the subscribers choose to block the edit, work continues.  The current contents of the cell are persisted (again, using jQuery’s data function) so that they can be restored later if the edit operation is canceled. 

Here is where things get a little tricky.  A handler is attached to the cell’s keyup event (keypress is currently bugged in jQuery for certain keys, particularly enter and escape).  Instead of attaching the function in-line though, I’m actually assigning it to a variable.  That’s so I can unregister the handler when I no longer need it. 

Inside the handler (which will be fired anytime the user presses a key while the editing the cell), nothing happens unless the user pressed the escape or enter keys.  When this happens, the editor is retrieved using jQuery’s dta function.  If the escape key was pressed, the editor is checked to see if it defines custom behavior that needs to be run when editing is canceled; if so, it is called.  After that, the old contents are restored.  If the user pressed enter, then they are indicating that they want to persist the value.  All editors must expose a getValue function that returns the new value.   The data item for the row is retrieved, and the new value is stored, and the element is updated.  A quick check is performed to see if the value actually changed.  Regardless of which key was pressed, the element is cleaned up.  The marker class is removed, and objects stored using jQuery’s data function are purged.  If the column was actually edited, the columnBound event is re-fired, so that interested parties can examine the new value.  Finally, the function unbinds itself from the keypress event so that it won’t be called again.

If that sounds complicated, that’s only because it is.  It took quite a while to get that part working, and I’m still not 100% satisfied with the code.  It *does* work though, in both Firefox and IE8, so I call that a win.

Next up is a simple helper that’s called after an element has entered edit mode:

base.beganEditing = function(column, tdElement) {

    tdElement.addClass("editing");

    base.liteGrid.$el.trigger("cellBeganEditing", [column, tdElement]);
}

It just adds the marker class and raises an event so that interested parties can keep up with what’s going on (remember, you can actually have modules that enrich the behavior of other modules by taking advantage of the event infrastructure that liteGrid is built on).

The next function is responsible for determining the appropriate editor for a cell:

base.getEditor = function(column) {

    //First, see if a custom editor is specified.
    if ("customEditor" in column) {
        return customEditor;
    }

    //If not, see if we have an editor for the type.
    var type = (column.type || "text");

    //All edit functions are in the form editType.
    type = "edit" + type.substr(0, 1).toUpperCase() + type.substr(1);

    //If the editor is defined, return it.  Looking up editors like this
    //allows for custom editors to be added on after the fact.
    if (type in base) {
        return base[type];
    }
    else {
        return function() { alert("No suitable editor found for this type: " + type); }
    }
}

First it checks to see if the column defines a custom editor.  I forgot to mention that you can do that.  So yeah, a column can define a custom function that handles its editing.  Next the editor type is built using the format “editorType”.  A quick check to see if the module defines this function is performed.  If it does, it’s returned, otherwise an alert is shown.  This flexible look-up of editors supports the ability to bolt-on new editors to an instance of the module that I described earlier.

Finally, we have the editors.  Due to this article already being hella-long, I’m going to show only the text editor.  The others are straightforward to implement:

base.editText = function(column, tdElement, dataItem) {

    //Used to eliminate scope issues.
    var editor = this;

    //Enables the editor on the element.
    editor.initialize = function() {
        editor.input = $("<input type='text' value='" + dataItem[column.field] + "' />")
        tdElement.html(editor.input);
        editor.input.focus();
    }

    //Invoked to save changes back to the underlying data item.
    this.getValue = function() {
        return (editor.input.val() || "");
    }

    editor.initialize();
}

The editor is broken up with its initialization logic wrapped in an inner-function, but it doesn’t have to be.  The only requirement is that an editor *must* expose a getValue function.  So, this editor simply builds a standard input box for the value being edited, adds it to the cell in place of its old contents, and sets the focus to it.  Getting the value is a simple matter of extracting the value out of the input later.  Remember that all cleanup is handled by the core of the module, so individual editors don’t have to worry about it (unless they want to).

Final Thoughts

After I completed most of the work on this module, James pointed out that I might have been able to leverage jEditable in my work.  I somehow neglected to check for tools to build from when I stared on this module, so it’s quite possible that I will rewrite this at some point to use jEditable instead of my custom editing scheme.  For now though, I’m fairly satisfied with the results.  As always, comments are very appreciated.

Tags: