Intro to jqGrid Part 5: Search

At this point we've created a basic grid, filled it with data, refined the display of our columns and added event handlers to handle multiselect options. We've used custom cell formatters, used a custom datatype function, and even added and populated a toolbar in the process. Now let's start looking at some things that aren't necessarily jqGrid specific, but incorporate them for use in our grid. How about a search?

jqGrid includes some things for doing data search, that will automatically build modal windows and stuff. But sometimes you want to format things your own way, or incorporate jqGrid for use within an existing interface. One of the advantages for us, using the datatype function, is that we can preprocess our postdata prior to the ajax request.

[More]

Anatomy of a Shopping Cart: A Usability Study

This little writeup is a usability study of cart layout and process in general. So many apps today are still sporting the 1999 click-and-reload interface, and times have changed. Users are tired of the old way, embracing the new, and if you're behind the times you could be losing clients fast.

Shopping Carts are funny, terrible things. The less intuitive they are, the higher the abandon rate, and yet you have to pack a ton of stuff in there. The trick is to anticipate user workflow, and match that as much as possible, without making the user work for it too much. This is the whole precept behind Don't Make Me Think.

First things first, create a method for detecting JavaScript prior to a user ever coming to the shopping cart, as it redefines the experience for the user entirely. If JavaScript isn't present, then it's either a bot, some non-Class A mobile device, or less than 1% of the desktop user. Surfing the web without JavaScript is even less of a chance than a user surfing without the Flash player. There's just too much of a user experience that is lost without it, and most standard users don't have a clue how to disable it to begin with.

You end up with two separate cart processes, one with JavaScript, and one without. "Two processes?" Well, you write a process with graceful degradation, that can do what needs to be done in either scenario, and with code reusability in mind. This writeup will go over the user with JavaScript enabled.

I've mocked up a simple, yet effective, cart design. This is just a wireframe model, but there's nothing here that can't be done with HTML and CSS. A user without JavaScript would have to step through the stages of placing an order, screen by screen. A user with JavaScript has to step through as well, but through JavaScript and Ajax, this user does most of their work right from the cart.

Below is the mockup, with an explanation of the different stages of process to follow:

This is pretty loose. Balsamiq only allows you to do so much, so color and highlights and icons give things a lot more pizazz, but this should give you a good idea. If you're unfamiliar with Balsamiq, it's a great tool for doing simple "pen on napkin" wireframe layouts during interface design. It's also a great way to draft quick mockups for clients without spending hours of unpaid time, and even more hours on revisions.

From the top down:

The only button at the top is the "Keep Shopping" button. A user has to verify all the details prior to checking out, so the "Checkout" button is at the bottom of the form. The "Clear" button is also at the bottom, giving the user a second chance not to clear out. A second "Keep Shopping" button is at the bottom of the page, to encourage the user to buy more.

Next up is the cart contents. A user, coming to the cart, wants to know what's in it first. This is the user's main focus, so registering for the site or entering credit details and stuff always comes after the cart itself.

There are a few different ways to handle actions on cart items. Here I show the 'Remove' checkbox. If a user hits the box, take the item out. Don't prompt 'Are you sure?' This annoys users. Of course they're sure, or they wouldn't have checked the box. Another way of handling 'Remove' is part of an action link list. There are already two other action links showing (Gift Wrap and Add to Registry), it would be easy to adjust the layout to use all action links instead (Remove at the top, Add to Wishlist, Add to Registry, with Gift Wrap last). Here's a tip though: Any of these actions will ultimately remove the item from the cart.

Being tabular in nature, it's best to use a table here. Unwritten rules of cart usability: the 'Item' is listed first, unless something small, like a checkbox, radio button, or icon precedes it. The cost (Qty * Price) is always last. This allows us to build a columnar layout of the money details. If you have line item discounts, you may show the math here too, to emphasize what the client is getting. In this mockup, it's all shown below.

Next, show the user some love. Discounts, Promos, Gift Cards and Coupons should follow the cart. 1) it makes it easier to calculate the Subtotal and 2) it gives the user a warm fuzzy to watch their Total decrease. If there were discounts on items, pull them out here (if you didn't do it above). Don't make a user refresh the page to get a new Total. Gift Cards and Coupons? Use Ajax to verify their validity and value, and apply it to the tally block right then.

If addresses aren't pre-populated, then the user hasn't logged in or registered or set up addresses yet. Our action depends on which scenario is the case. If the user hasn't logged in or registered, then we show them a login box, with a register button. If they login, we go ahead with the rest of the order process. If they must register, handle that registration in a modal popup and then update your area. If the system allows for anonymous users (those who don't want, or have, to register with the site), or a registered user hasn't setup addresses yet, then we display "add address" links to the display that can load modal forms to collect the necessary details.

You can't tally Tax unless you know you need to collect it. If you don't have an address yet, don't show the Tax line item. If you get an address, and tax does need to be applied, add it in right away, and highlight it, and the Total change, so there's no surprises.

And, once you have an address, then you already know the zip/postal code the order is going to. Once a Shipping Method is selected we can lookup the shipping cost via Ajax. And, if the purchase is a download, then there's no reason to show them the Shipping line item at all. Avoid the "Estimated Shipping" thing, just give them the info once you have it. If need be, show them text ("We need your address and Shipping Method to show Shipping Cost") to help push them through the order process.

Last is the Payment Method. When a user selects the Payment Method, then the input fields for this area should change to the appropriate fields for that method. Don't show them fields for every possible option, just those for the option they've chosen.

That's it. By streamlining your Cart and Checkout process to match user workflow, you should see a lower abandon rate and higher sales. There's a lot of code involved, and both client-side and server-side data validation is a must, for security purposes, but the end gain makes the effort more than worthwhile. What are your suggestions for improving Cart usability? Give me your feedback below.

Intro to jqGrid Part 4: Event Handling

By now our demo is really beginning to flesh out a bit. You've probably created your own grid, read through some of the extensive documentation, and started to figure a few things out on your own. In our series, we've created a basic grid, populated it with remote data, and refined our column configuration a bit. This is a lot of information, encompassing in depth explanations on many key basic grid configuration options, writing our paging query, ajax data handling, and more. Now, let's get into some 'grid' stuff that might not be so obvious.

Toolbars

First, we're going to set ourselves up for the future. We're going to want to see some data, but in order to do so we're going to need some buttons to click on. This is a good time to talk about jqGrid's toolbar implementation. jqGrid makes it easy to add toolbars to it's display, but it's a bit of a departure from standard JQuery plugin behavior, as it doesn't allow you to create a toolbar from an existing DOM element, so you have to use script to define and create your toolbar and it's associated elements. The first thing you have to do is add the configuration attribute to your jqGrid config object:

jqGridDemo.js - Toolbar Config

view plain print about
1grid.jqGrid({
2    ...
3    toolbar:[true,"top"],
4    ...
5});

The toolbar config option takes a two element array for an argument. The first (boolean) element enables the toolbar. The second element defines the toolbar's placement. This value can be "top", "bottom" or "both". Adding this configuration attribute will cause jqGrid to automatically create a div for each toolbar defined. It uses a specific naming convention, every time, to create the toolbars: top - "t_" + grid element's id, bottom - "tb_" + grid element's id (i.e.: "t_gridTest"). If you ran your template now, you would see that an empty toolbar has been added to your grid display:

Now that we have a toolbar, we need to add some buttons. JQueryUI has a button object, for nice consistent buttons, so we'll create the DOM elements needed on the toolbar first, then make them into JQUI buttons.

jqGridDemo.js - Toolbar Buttons

view plain print about
1$('#t_gridTest').append('<button id=\"addButton\">Add</button><button id=\"exportButton\">Export</button>').addClass('customToolbar');

jqGridDemo.js - JQUI Buttons

view plain print about
1$('button#addButton').button({icons: {primary: 'add'}}).click(function(ev){
2    ev.preventDefault();
3    
4    return false;
5});
6
7$('button#exportButton').button({icons: {primary: 'page_white_go'}, disabled: true}).click(function(ev){
8    ev.preventDefault();
9    
10    return false;
11});

Here we've created an Add and an Export button on our toolbar, while also adding a new CSS class (customToolbar). Then I made JQUI buttons of our new toolbar buttons, applying appropriate icon classes (defined in our css file). I've also disabled the Export button for the moment, but we'll go into that later.

OK, now we have a toolbar with some nice, pretty buttons on it, let's start talking about some more grid-centric things, like selection models. Data grids serve a wide range of purposes. In the basic model, we were just viewing data, even if we did allow for sorting and paging. We could've just created a table in DOM and used jqGrid's tableToGrid method to make a simple view table. But, chances are you're writing a Data Grid implementation to interact with data. And a common action is to select a record (or records) to interact with.

Multiselect

There are, essentially, three types of selection actions with a Data Grid: individual record selection, multiple record selection, and specific cell selection. The act of any of these selections is an event, and jqGrid includes the ability to add custom Event Handlers directly to the grid configuration. In our implementation, I want to show working with multiple selections, so we need to add a few configuration options. First, let's setup for multiselection:

jqGridDemo.js - Multiselect Configuration

view plain print about
1grid.jqGrid({
2    ...
3    multiselect: gridMultiSelect >
0,
4    multiselectWidth: 25,
5    ...
6});

These are direct grid configuration options. The first, multiselect, is a boolean value that, if true, will automatically create a column of checkboxes on our grid. The second option, multiselectWidth, defines the width of that checkbox column. Notice that I've tied the multiselect attribute to the gridMultiSelect global variable I added a few posts back. If we change that global variable now (gridMultiSelect = 1) you will see it's effects when you reload the page.

And now we have checkboxes for multiple record selection. The user can select, or deselect, records at will, or even 'select all' by clicking the checkbox in the header column (NOTE: this only selects all currently loaded records in the grid). That gridMultiSelect variable is also an offset value, when calculating column positions in our JSON remapping. The inclusion of the checkbox column (multiselect) requires us to shift our positions by one space.

Now there are two things we have to think about: 1) What do we do when a user clicks a row (or selects 'all')? and 2) How do we know what's been selected?

Well, there are all kinds of things you can do with the records selected. What we'll talk about, in our example, is correcting a small issue with jqGrid itself. Let me explain. jqGrid keeps an array of id's, for all selected records, in the read-only grid attribute selarrrow. You can programmatically access those at any time by grabbing that variable from jqGrid.

view plain print about
1grid.jqGrid('getGridParam','selarrrow');

Id's

But if you play with it a bit you'll discover a few things. The first thing you'll figure out is that we aren't (yet) getting the ID of our records, getting a generic rowcount for our id instead. This is because we've used the function datatype, and remapped our JSON to our column configuration, so the id attribute of the jsonReader is basically being ignored. (I've presented a fix for this to the jqGrid team, which they will hopefully implement in the future.) For this reason, we have to set a key in our column model. This will setup an internal keyIndex variable that jqGrid uses in it's processes. Under normal circumstances, the key attribute would be defined on your ID column in the column model. When remapping JSON output, though, this isn't how this would be accomplished. You would need to review your incoming JSON, figure out the index of the ID column in the response, then apply that key attribute to that column index in the column model configuration. In our example the ID column is still first, lining up correctly, but in more complex column models and queries this can be different. Here's how you add that to the column model configuration.

jqGridDemo.js - Column Model Configuration

view plain print about
1colModel: [
2    {name: 'ID', hidden: true, key: true},
3    ...
4],

The importance in this can be viewed in the output. Here's a sample of the before and after difference in what jqGrid generates:

Grid Row Output - Before Key Definition

view plain print about
1<tr id="1" class="ui-widget-content jqgrow ui-row-ltr" role="row" tabindex="-1">

Grid Row Output - After Key Definition

view plain print about
1<tr id="FF318BAB-3048-71C2-17E1634637074ECF" class="ui-widget-content jqgrow ui-row-ltr" role="row" tabindex="-1">

Why is this important? When you go to the next page of records, if you hadn't applied the key then the first record displayed would also show an id of 1. And this now brings us to the bit that we're going to address with jqGrid. If you select a few rows, pull the selarrrow, then page and select some more and check again, you will see that jqGrid isn't tracking id's across paging requests. This means that, if you go back to page 1, your original selections are no longer checked. For some this may be ok, but many users will expect different behavior. This is part of a developer's life, is anticipating user behavior, and adjusting to meet good usability guidelines.

Event Handlers

So, what is expected behavior? If a user selects a record, then goes to another page, and then returns to the original page, then the user will expect their selections to have been maintained across paging requests. This becomes especially important to users working with very large amounts of data. So, how do we handle this? Well, we setup an Event Handler for our selections, and programmatically control it. First, let's create another global variable to hold selected id's.

view plain print about
1var gridCols = {set:false},
2    gridMultiSelect = 1,
3    selArr = [];

This is just an array, just like jqGrid uses internally. Next thing we'll do is setup some Event Handlers. Any time a selection (or deselection) is made, then we need to control the contents of our new selArr array. First we'll setup a method to apply to our onSelectRow grid Event Handler.

jqGridDemo.js - rowSelectionHandler

view plain print about
1var rowSelectionHandler = function (id, status) {
2    // process code here
3};

jqGridDemo.js - Grid Configuration Event Handler

view plain print about
1grid.jqGrid({
2    ...
3    onSelectRow: rowSelectionHandler,
4    ...
5});

A review of jqGrid's Events documentation shows that the onSelectRow attribute will apply an event handler, and passes two arguments: id - the record id of the row being selected, and status - a boolean value noting selection (true) or deselection (false). jqGrid's onSelectAll attribute is similar, passing idArr - an array of selected row id's, and status - if all were selected or deselected. We'll setup a handler for that as well.

jqGridDemo.js - selectAllHandler

view plain print about
1var selectAllHandler = function (id, status) {
2    // process code here
3};

jqGridDemo.js - Grid Configuration Event Handler

view plain print about
1grid.jqGrid({
2    ...
3    onSelectAll: selectAllHandler,
4    ...
5});

Now, it may be that we'll want to use these two methods for something other than keeping track of our new array. Ultimately, the work for handling our new array is the same for either, we just have to code in handling the difference between a single id being passed, or an array of id's being passed. We'll setup a separate method for the array manipulation, and call it from our new handler methods.

jqGridDemo.js - Handlers and selectionManager

view plain print about
1var rowSelectionHandler = function (id, status) {
2    selectionManager(id, status);
3 // anything else
4};
5
6var selectAllHandler = function (idArr, status) {
7    selectionManager(idArr, status);
8 // anything else
9};
10
11var selectionManager = function (id, status) {
12    // was it checked (true) or unchecked (false)
13    if(status){
14        // if it's just one id (not array)
15        if(!$.isArray(id)){
16            // if it's not already in the array, then add it
17            if($.inArray(id,selArr) < 0){selArr.push(id)}
18        } else {
19            // which id's aren't already in the 'selected' array
20            var tmp = $.grep(id,function(item,ind){
21                return $.inArray(item,selArr) < 0;
22            });
23            // add only those unique id's to the 'selected' array
24            $.merge(selArr,tmp);
25        }
26    } else {
27        // if it'
s just one id (not array)
28        if(!$.isArray(id)){
29            // remove that one id
30            selArr.splice($.inArray(id,selArr),1);
31        } else {
32            // give me an array without the 'id's passed
33            // (resetting the 'selected' array)
34            selArr = $.grep(selArr,function(item,ind){
35                return $.inArray(item,id) >
-1;
36            },true);
37        }
38    }
39    $('#t_gridTest button#exportButton').button((selArr.length >
0)?'enable':'disable');
40};

I've tried to comment the code above to explain our new selectionManager method. The only line I haven't explained is the last, which either enables or disables the Export button in the toolbar, depending on if any items are selected or not. Now, if you made selections, paged through and made other, deselected items, even selected 'all', you could look at the selArr variable and see that we are now tracking all selected records across paging requests. The only thing missing is that these records aren't selected (visually) when you return to a page. We can rectify that by applying a handler to our gridComplete attribute.

jqGridDemo.js - - Grid Configuration Event Handler

view plain print about
1grid.jqGrid({
2    ...
3    gridComplete: gridLoadInit,
4    ...
5});

jqGridDemo.js - gridLoadInit

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};

jqGrid's gridComplete attribute allows us to define an Event Handler that fires once data has loaded into the grid (this is not to be confused with loadComplete, which occurs after every server request.) What we're saying here is, after the data is rendered in the grid we will loop the selArr array and 'check' any id's that match any records displayed in our grid. This gridLoadInit method, that we've created, will now run anytime the grid's data is reloaded (initial load, paging requests, sorting, etc).

And so we've created a solution to rectify a small oversight within jqGrid. In the process we covered the importance, and the process, behind properly identifying a key column, in setting proper row id's, how to add a checkbox column through the multiselect attribute, how to apply event handlers to our grid, as well as adding a toolbar and filling it with controls. Next post we'll get to binding event handlers to our Action column icons, and search for some other nuggets to impart. Until then, I hope this all helps someone, and sample code is located in the Download link at the bottom of the post.

2011 In Review, and the View for 2012

My, how time flies when you're having fun! It seems like only yesterday that I was welcoming in 2011, and now we're here a year later. So many things have happened in the last year, and rereading that post I see that I missed some things I should've done, but let's take a look in retrospect.

I wrote 27 blog posts in 2011. This is nothing, compared to guys like Ray Camden or Ben Nadel, but for me it was quite a bit, especially when you consider that between March and August I released only one post. Very early in the year, I began a series on creatingmany sites with one codebase. In the process, the series has evolved to contain a fairly detailed primer in ColdFusion application architecture (because of it's importance to this process), has currently spanned 8 separate posts, and was even referenced by Sean Corfield in his great presentations on the same topic. 2012 will see the completion of that CF app discussion, and gradually move it back to the MSOC topic itself, as there is still a ton to talk about there, and a lot of interest in the topic. I also began a series on the jqGrid JQuery plugin. jqGrid is another Data Grid visualization tool (I have now written about three, including Ext JS and DataTables), and is a clear choice for those who must use JQuery. (To be fair, JQueryUI is working on a grid component, but they are still behind the curve, and way behind Sencha.) Finally, one common thread seen in the majority of my posts, is how much I've embraced cfscript. I wrote a lot of things, on a variety of topics, but most of my code examples were pure scripted examples.

Now let's talk about some other departures from the norm for Cutter.

You did not see a lot of content around Ext JS. In fact, I stopped writing Ext JS books. This is not, in any way, a reflection on my feelings for Ext JS. I still believe that Sencha has built one of the best client-side libraries for web application development. In evaluating the overall ROI, I realized that I was writing more for the community than the money, and that my reach was greater through my blog, while giving me flexibility on when and what I deliver from a content standpoint. That said, I didn't have a single project this year that used Ext JS, so had very little time to experiment and write about it. This year, I'm going to expand on a personal project, and get back to some great Ext JS content for my readers.

You, also, did not see me speak at any conferences this past year. Nor at any user group meetings. This wasn't because I didn't want to, but because of some more personal reasons. I'm not going to go in depth here, other than to say that I've had some long standing health issues that required me to have some surgery done on my mouth. (Mark Drew is making a joke right now...) Aside from the fact that this has been very costly (chewing up any conference/travel budget), it also meant that my speech has been affected for a good part of the year. Thankfully this experience is (mostly) over now, and I hope to get back to presenting sometime this year. Any user group looking for a speaker this year, please contact me through the Contact link on this blog.

One group I am hoping to speak to this year is the Northeast Florida CFUG. I have to call Mike back, but he's looking to get things kicked off again, and I want to help it be successful. If you're in or around the Jacksonville area, make sure to keep an eye on the site for upcoming events.

One other thing I'm looking to do is to migrate all of my projects into GitHub. I've been using Git at work, and I am loving it, and I think combining GitHub with RIAForge is a great way to promote the terrific technologies we work with every day. I will make the time, I promise.

This comes to the final discussion of this post, Adobe. I again had the pleasure of being an Adobe Community Professional this past year. Due to my health issues, I didn't get to do everything I would've wanted to this year, but I've tried to be a good supporter. There are some fabulous things coming in ColdFusion Zeus and, by extension, to ColdFusion Builder as well. There has been a lot of hub-bub over Adobe's communications flubs regarding Flash, mobile, and Flex. I've avoided much of the discussion, other than to say "be patient and watch". Flash isn't going away, and neither is Flex. HTML 5 is a beautiful thing, if you aren't developing desktop browser applications (i.e. You're only writing for mobile/tablet development). There, that is my whole contribution to that discussion. Give it a rest.

2012 will be a fantastic year. Set yourself some clear, definable goals. Break them down, step by step, and write the steps down on paper. Each successive step, print out in large letters and place it somewhere where you will see it each and every day. Set yourself up to succeed, and you will. Have a great year, everyone, and I can't wait to hear what you have planned for 2012.