So far we've covered the base grid configuration, populating the grid with remote data, controlling layout by refining the column model, and given an example of event handling by binding a search form to our grid. This covers most of what the average developer might need to know about working with jqGrid, but I've been promising to tell you how to work with the icons we setup in our action column.

jqGrid provides the ability to add predefined 'action' icons directly. That said, it's a small, predefined set of actions, and they build out dialogs and forms according to configuration details. This might be nice for prototyping, or small applications, but most of us already have interfaces for editing and stuff. I like finer control over the 'look' of my interfaces, and prefer to use stuff that's already built, if we have it. So, we make our own 'action' icons, and attach our own custom events.

First, let's look again at the custom cell renderer that we wrote for our 'action' column:

view plain print about
1$.extend($.fn.fmatter, {
2    actionFormatter: function(cellvalue, options, rowObject) {
3        var retVal = "<span class=\'icon-trigger action-trigger pencil\' rel=\'" + cellvalue + "\' \/>";
4        retVal += "<span class=\'icon-trigger action-trigger delete\' rel=\'" + cellvalue + "\' \/>";
5        return retVal;
6    }
7});

Yeah, this one was pretty simple. All it did was add two icons to the display and apply the cell value to the rel attribute. The cell value, in our case, is the ID column, which is the unique reference you would want for this type of scenario. Now, the first thing you might notice, in reviewing the current output, is that something looks a little off. Can you spot it?

If you look close you can see it. Every other row the icons and text are a little washed out. This is because of the altRows option we used in our jqGrid configuration. Removing that option will remove the alternate row highlighting, which is causing the washout effect. Here, let's run a little experiment. First, remove the altRows attribute from our grid configuration. A quick reload will show you the washout is gone, just like I said. Now, let's create a new style declaration in our stylesheet.

view plain print about
1table#gridTest tr:even td { background-color: #F5F5F5; }

If you reload the page you'll notice that didn't do anything. Why is that? Well, good question. Maybe it's because the rows don't exist until after the request is made. Let's take a different tact. Let's change that style declaration, making it define a class.

view plain print about
1.evenRow { background-color: #F5F5F5; }

Next, let's add a line to the end of our gridLoadInit method.

view plain print about
1$('table#gridTest tr:even td').addClass('evenRow');

Now refresh your grid. You get the grid highlighting without the washout effect. Cool! You might remember that, in a previous post, the gridLoadInit method is attached to the grid's gridComplete configuration option, and fires each time data is loaded into the grid. That method becomes very important, as it allows you to do things with the records/rows once they are loaded. "But, what does this have to do with the 'Action' column stuff?" Well, that leads to our first method of attaching event handlers to our action icons.

Bound Handlers

For a number of reasons, I'm using an older version of JQuery in these posts. You do have access to .live() binding, but sometimes it's nice to really take control of when you are binding/unbinding events. Basically, we want to apply event handlers to every row after it loads, and remove those bindings before we load new records. We'll start by creating a new method that we can call from within our gridLoadInit method. You could put all of the code directly in the method, but let's break it up a little to make maintenance a little easier. We'll write a new bindActionHandlers method inside our $(document).ready().

view plain print about
1/*
2 * FUNCTION bindActionHandlers
3 * This method is called within the gridLoadInit() method, and is used to
4 * bind event handlers to the action icons of new records
5 */

6var bindActionHandlers = function () {
7    
8};

Here we'll place all of our code to create new event bindings for our action columns. Let's start by binding a click handler to our delete icons.

view plain print about
1var bindActionHandlers = function () {
2    // Delete icon binding
3    $('span.delete[class*="action-trigger"]', grid).bind("click", function(e){
4        e.preventDefault();
5        
6        return false;
7    });
8};

What we're saying here is a) apply this method as the click handler to delete icons inside our grid that include the action-trigger class, and b) prevent any default click action and return false when we're done. Now, let's think about this a minute. What would we expect a user to see and do when they are trying to delete this record? Well, we probably want to make sure that they really want to remove the record. Let's build them a little confirmation dialog. First, we probably need to know which record they're trying to remove. We probably also want to show them the title of the record in the confirmation. Let's set up some function local variables.

view plain print about
1var bindActionHandlers = function () {
2    // Delete icon binding
3    $('span.delete[class*="action-trigger"]').bind("click", function(e){
4        e.preventDefault();
5        var row = $(this).parent().parent(),
6            rowId = row.attr("id"),
7            recId = $(this).attr('rel'),
8            title =    grid.jqGrid('getCell',rowId,'Title');
9
10        return false;
11    });
12};

OK, this gets us the id of the record, along with the title. Now we'll build a little confirmation box.

view plain print about
1var bindActionHandlers = function () {
2    // Delete icon binding
3    $('span.delete[class*="action-trigger"]').bind("click", function(e){
4        e.preventDefault();
5        var row = $(this).parent().parent(),
6            rowId = row.attr("id"),
7            recId = $(this).attr('rel'),
8            title =    grid.jqGrid('getCell',rowId,'Title');
9        // Create a dynamic dialog, that is destroyed when closed
10        $('<div>').dialog({
11            title:'Delete Confirmation',
12            width:425,
13            height:200,
14            modal:true,
15            create: function(){
16                $('span.ui-icon-closethick').html("");
17            },
18            close:function(){
19                $(this).dialog('destroy');
20            },
21            buttons:[
22             {text:'OK',
23             click:function(){
24                 deleteItem(rowId,recId,$(this));
25             }},
26             {text:'Cancel',
27             click:function(){
28                 $(this).dialog('close');
29             }}
30            ]
31        }).html("Are you sure you want to delete the following?:<br />"+title);
32        return false;
33    });
34};

Now we create a JQueryUI dialog on the fly, to use as a confirmation box. We setup OK and Cancel click handlers, and dynamically set the text of the dialog to reflect the title of the record being deleted. We also call adeleteItem() method, to delete the record. We'll build that later. Now that we've put all of this together, all we need to do is add a call to the bindActionHandlers() method to the end of our gridLoadInit() method.

view plain print about
1var gridLoadInit = function () {
2    // if the 'selected' array has length
3    // then loop current records, and 'check'
4    // those that should be selected
5    if(selArr.length >
0){
6        var tmp = grid.jqGrid('getDataIDs');
7        $.each(selArr, function(ind, val){
8            var pos = $.inArray(val, tmp);
9            if(pos > -1){
10                grid.jqGrid('setSelection',val);
11            }
12        });
13    }
14    
15    $('table#gridTest tr:even td').addClass('evenRow');
16    bindActionHandlers();
17};

Now let's reload the page, and click the delete icon of our first row.

The downside to dynamically applying your event handlers like this is that you also need to unbind handlers before you refresh your data. We already stubbed out a method for this in our $(document).ready(), gridUnloader.

view plain print about
1/*
2 * FUNCTION gridUnloader
3 * Called from within the populateGrid method, this method is to unbind
4 * any event handlers created during grid load, by the gridLoadInit method.
5 */

6var gridUnloader = function () {
7    
8};

After that you just add the code to unbind the click handler

view plain print about
1var gridUnloader = function () {
2    $('span.delete:not("disabled-trigger")', grid).unbind('click');
3};

This says unbind any click handler applied to a grid's delete icons that don't have a class of disabled-trigger. ("Hey Cutter! What's That!" All in good time...) Now that you have the method, all you have to do is call it from the correct point of context. Within the data grid config option, which handles all requests for grid data, we had a piece of code that runs on the first request (to map column positions). This is in a conditional that basically says "if not set, then set it". We put an else clause here, to now see "if it is set, then we're reloading the grid. We know if we're reloading we need to unbind all our row level event handlers, so this is where our call to gridUnloader will go.

view plain print about
1var populateGrid = function (postdata) {
2    $.ajax({
3        url: '/com/cc/Blog/Entries.cfc',
4        data: $.extend(true, {}, postdata, {search: $.toJSON(scrubSearch())}),
5        method:'POST',
6        dataType:"json",
7        success: function(d,r,o){
8            if(d.success){
9                
10                // If loading for the first time, let's find out to which
11                // array positions our columns map.
12                if(!gridCols.set){
13                    columnSetup(d.data);
14                } else {
15                    // Unbind any bound event handlers in the current grid data
16                    // Only necessary after the first grid load
17                    gridUnloader();
18                }
19
20                grid.jqGrid('setGridParam',{remapColumns:[
21                    gridCols['ID'] + gridMultiSelect,
22                    gridCols['TITLE'] + gridMultiSelect,
23                    gridCols['POSTED'] + gridMultiSelect,
24                    gridCols['VIEWS'] + gridMultiSelect
25                ]});
26                grid[0].addJSONData(d);
27            } else {
28                console.log(d.message);
29            }
30        }
31    });
32};

Let's stop and take a minute to build that deleteItem method, and the remote method it will call. There's a few things that need to happen.

  1. Make an ajax call to remove the record
  2. If the call is successful, remove the row from the grid
  3. If the call fails, show a message to the user
  4. Close the Delete Confirmation dialog box

Let's start by writing a quick dialog configuration object for our generic error dialog. We'll place this inside our $(document).ready() statement.

view plain print about
1// Error dialog box config
2$('div#grid-dialog-error').dialog({
3    width:400,
4    autoOpen: false,
5    modal: true,
6    create: function(){
7        $('span.ui-icon-closethick').html("");
8    },
9    buttons:{
10        "OK": function(){
11            $(this).dialog("close");
12        }
13    }
14});

We'll use this in a few places going forward. Right now, let's write our ajax call for removing a blog entry.

view plain print about
1/*
2 * FUNCTION deleteItem
3 * This method is for deleting records and removing the
4 * corresponding grid entry
5 * @rowId (int) - ID of the row to be removed from the grid
6 * @recId (string) - The UUID of the blog entry to be removed
7 * @dlg (object) - JQuery element of the JQUI dialog
8 */

9var deleteItem = function (rowId, recId, dlg) {
10    $.ajax({
11        url: '/com/cc/Blog/Entries.cfc',
12        data: {
13            method: 'deleteEntry',
14            recId:recId,
15            returnFormat: 'json'
16        },
17        dataType: 'json',
18        success: function(d, r, o){
19            if (d.success) {
20                grid.jqGrid('delRowData',rowId);
21                dlg.dialog('close');
22            }
23            else {
24                $('span#grid-dialog-error-message').html(d.message);
25                $('div#grid-dialog-error').dialog('open');
26            }
27        }
28    });
29};

This will make an ajax call to remove the record. If the call is successful, then the row will be removed and the dialog closed. If it fails, then our new error dialog will open to tell us why. This tells us a little about how we need to form our remote method.

view plain print about
1/**
2 *    FUNCTION deleteEntry
3 *    Used to remove entries from the system
4 *
5 *    @access remote
6 *    @returnType struct
7 *    @output false
8 */

9function deleteEntry(required string recId){
10    LOCAL.retVal = {"success" = true, "message" = "", "data" = ""};
11
12    // BEST PRACTICE: You'll want to verify that the user has the right to do this. Normally, that would go here.
13
14    LOCAL.sql = "DELETE FROM tblblogentries
15                 WHERE id = :recId";
16    LOCAL.q = new Query(sql = LOCAL.sql);
17    LOCAL.q.addParam(name = "recId", value = ARGUMENTS.recId, cfsqltype = "cf_sql_varchar");
18    try {
19        // You would uncomment the following line to actually remove records, and remove the throw statement
20        // LOCAL.q.execute();
21        throw (message = "Intentional Exception: You didn't really think I'd delete entries, did you?", type = "custom_err", errorCode = "ce1001");
22    } catch (any excpt) {
23        // In testing, and with the .execute() commented out above, comment out the next line to watch the grid remove a row
24        LOCAL.retVal.success = false;
25        LOCAL.retVal.message = excpt.message;
26    }
27    return LOCAL.retVal;
28}

Yes, I didn't really delete anything. The code is there, but we're not killing entries today. This does give you the basic idea on how this all works though. Trying to delete an item in the grid now will show you a confirmation dialog. When you click on OK the ajax call is made. Right now, we intentionally throw an error. The success marker causes the error dialog to display with the message. If you comment out the first line of the catch, in this method, and re-run your test, you will see the confirmation dialog close, and the deleted record's row removed from the grid.

Old School Event Handler

Nowadays, it's standard practice to bind event handlers at runtime. It's even referred to as best practice. That said, there are issues with these new methods, particularly in making sure to unbind events from items you're removing from the DOM (or when they aren't needed at all). Binding events at runtime has runtime implications, and can cause memory leaks if not carefully controlled. For this reason, sometimes it just makes more sense to take it on the old school way. To demonstrate, let's take a different tact with how we handle our edit icons.

view plain print about
1$.extend($.fn.fmatter, {
2    actionFormatter: function(cellvalue, options, rowObject) {
3        var retVal = "<a href=\'javascript:void(0)\' onclick=\'editEntry(\"" + cellvalue + "\")\'><span class=\'icon-trigger action-trigger pencil\' rel=\'" + cellvalue + "\' \/></a>";
4        retVal += "<span class=\'icon-trigger action-trigger delete\' rel=\'" + cellvalue + "\' \/>";
5        return retVal;
6    }
7});

We put a standard anchor tag in there, with the onclick attribute calling a new method, editEntry, to which we pass the Entry ID (cellvalue). It's not elegant, but it works. To prove that, let's build out our editEntry method. We don't want to build a full Blog entry editor in this blog post, but we can create a quick page to load in a dialog via an ajax call.

view plain print about
1<cfsetting enablecfoutputonly="true" />
2
3<cfparam type="string" name="FORM.recId" default="" />
4
5<cfif !Len(FORM.recId)>
6    <cfoutput>You must supply an ID of a record to edit!</cfoutput>
7    <cfabort />
8</cfif>
9
10<cfoutput>
11    <p style="font-weight:bold;">You are editing #FORM.recId#
12</cfoutput>
13
14<cfsetting enablecfoutputonly="false" />

This is just a simple template that takes a single POST variable (recId) and outputs it on the page. Now all you need is the ajax call to render this in a dialog box.

view plain print about
1var editEntry = function (recId) {
2    $('<div id="recordEdit">').dialog({
3        title: 'Entry Editor',
4        modal:true,
5        width: $(window).width()*.6,
6        height: $(window).height()*.6,
7        create: function(){
8            $('span.ui-icon-closethick').html("");
9        },
10        open: function(){
11            var dlg = $(this);
12            // Get the summary for the selected lesson
13            $.ajax({
14                url: '/edit.cfm',
15                type: 'POST',
16                data:{
17                    recId: recId
18                },
19                dataType:'script',
20                success: function(d, r, o){
21                    dlg.html(d);
22                }
23            });
24        },
25        close:function(){
26            $(this).html('').dialog('destroy');
27            setTimeout('$("#recordEdit").remove();',100);
28        },
29        buttons:[{
30            text:'Close',
31            click:function(){
32                $(this).dialog('close');
33            }
34        }]
35    });
36};

Here we create a div element on the fly. Pay special attention to our close config option, where we have to pull off some trickery to destroy the JQueryUI Dialog, and remove the div from the DOM. This is very important, because every time we click on an edit icon it is going to create and open a new dialog. In the open config option we make an ajax call to retrieve edit.cfm, passing it the record ID (recId) of the record we want to edit. We place the return of the request in the content of the JQueryUI Dialog, which we sized to take up 60% of the window size.

Wrap It Up

At this point I think we have a pretty good "Intro" to jqGrid. If there's some critical piece of info you think I have missed, please feel free to let me know through the Contact link below. Sample code for our application can be found below, from the Download link. As always, give me your feedback and questions.