Look What Complete UI’s Grid Can Do, part two | April 1st, 2009
I know everyone’s been waiting for this post with bated breath. The exciting continuation to my post last week is finally here! To recap, on a recent Nitobi project that I worked on, we needed to dramatically improve the user and management experience for the site administrators when using the ‘Control Panel’ part of the website we were building for them. The previous administration panels were whipped together quickly and badly maintained. If you want to see before and after pictures, refer to my previous post.
As I mentioned last week, I was able to borrow fellow Nitobian and lead UX Designer Chris to help with the redesign for this project. Coming from a software development background, I did not realize how little thought I actually put into designing and polishing the user experience of software that I developed, until I worked with Chris. It’s a completely different way of looking at not only software but anything that one makes, creates or designs. I would highly recommend it to any software developer - if you have the time and ability to work one-on-one with an experienced designer, it can be quite a humbling but eye-opening and educational experience.
A quick recap of what we were trying to achieve: the administration page that we used Complete UI’s Grid in is what I’ll call the ‘Resource Administration’ page. At a high level, there is content on the site that needs to be moderated, often edited/tweaked, or deleted. The resource administration page was structured so that the user could search for particular resources, select one resource from a basic grid of results, and then edit that resource’s information using a form that is populated with the selected resource’s information at the bottom of the page. One of the first things Chris and I did was we went through the old administration pages and did some common tasks that our administrators have to do. For each task, we analyzed the workflow and asked ourselves, “Is this the best way to get the job done?” The answer was simply no; we could improve the user experience drastically and not only reduce redundant steps but also improve the page responsiveness. Both of these factors combined to yield a fast and painless (or at least much less so than before) experience for our site administrators. One new feature that we added to our administration page was batch processing - the ability to select many ‘resources’ at once and apply the same operation to all of the items. This is what I’ll be going into detail about in this post.
Batch Processing in Grid
Overview
Implementing batch processing in Grid is not difficult. There are basically three parts to getting it working:
- Set up your Grid instance to allow multiple row select - a very cool feature.
- Add some JavaScript functions to your page that grab data from the selected rows in Grid and initiate an XMLHttpRequest (or XHR for short) to some server-side method that runs the batch operation.
- Finally, write the server-side methods that run the batch operations.
I won’t be able to help much with the last point, but I’ll include a short overview of one of my batch server-side methods for context and example purposes. I’ve also put together a small example archive with a working page set up to show off how this works - only uses local data, though, not real server-side methods, but I hope it’s enough to get the idea across.
Implementation
Grid Instance
Man, I love customizing components declaratively - it’s so easy. To get multiple row select enabled in Grid, simply add this to your <ntb:grid> declaration: rowselectenabled=”true” multirowselectenabled=”true”. That’s it. Easy, huh? More features are available with Grid - too many to name. Check out the HTML Tag Reference over at our Complete UI Wiki to see what other features are available.
JavaScript
So you can now select multiple rows in Grid by Control+clicking. Sweet. Next step is to hook in our batch process initiation. I did this with a simple <button> element. It triggers a JavaScript function when the onclick event is triggered. For example: <button onclick=”batchDelete(’myGridID’);return false;”>Delete</button>. When you click this button, the ‘batchDelete’ JS function is called. Here it is, in all its glory:
function batchDelete(gridID) {
var g = nitobi.getComponent(gridID);
if (g.selectedRows.length > 0) {
// Iterate over selected rows.
var IDs = [];
var numSelected = g.selectedRows.length;
if (confirm('You sure you want to delete these, chief?')) {
for (var i=0;i < numSelected; i++) {
var xi = g.selectedRows[i].getAttribute('xi');
var ID = g.datatable.xmlDoc.selectSingleNode('//ntb:e[@xi=' + xi + ']').getAttribute(g.fieldMap['ID'].substring(1));
IDs.push(ID);
}
// Create and send off XHR request.
var xhr = new nitobi.ajax.HttpRequest();
xhr.handler = "delete.ashx?ids=" + IDs.join(",");
xhr.async = true; // async is true by default
xhr.completeCallback = function(evtArgs) {eval('var message='+evtArgs.response+';');alert(message.status);};
xhr.get();
}
} else {
alert('Select something first, champ!');
}
}
I know, I know, “WTF is this???” Hold on, let me go through it line-by-line and it will all become clear, young Padawan.
function batchDelete(gridID): The batchDelete function takes as a parameter the ID of the Grid instance we’re hooking this up to. This is done so we can easily re-use this function for different Grids/pages.var g = nitobi.getComponent(gridID): We set a reference to the Grid’s JavaScript object, since we’ll be referring to it back later in the function.var IDs = [];var numSelected = g.selectedRows.length: We set the IDs variable to an empty array - we’ll be using it to accumulate the IDs of the selected rows. The numSelected variable just tells us how many rows are selected.- The next conditional will just bring up a confirmation dialog asking the user if he or she really wants to delete the selected items.
- The for loop here is where the Complete UI magic happens. The loop iterates over the selected rows, and does this:
var xi = g.selectedRows[i].getAttribute('xi'): The rows in the Grid and their related DOM elements have an ‘xi’ attribute - this is a zero-based row index. We set it to a variable and use it in the next step…var ID = g.datatable.xmlDoc.selectSingleNode('//ntb:e[@xi=' + xi + ']').getAttribute(g.fieldMap['ID'].substring(1)): This is the tough one. First, we call selectSingleNode on the Grid’s datatable object’s XML Doc object. This is just an XML document that houses all of the Grid content’s data. The XPath expression passed into selectSingleNode will select a ntb:e node (think of this as a row object - one ntb:e node per Grid row) with the XI attribute matching to the current selected row that we are processing. Next, we call getAttribute() on this ntb:e node to retrieve the ID attribute from the data. The fieldMap bit inside the getAttribute call is necessary, as the attributes of the ntb:e nodes are named with alphabet letters - starting from a up to z (and then using two letters beyond that, i.e. aa, ab, etc.). However, when you set up your get handler for the Grid, you create and name the data fields whatever you want. That’s where fieldMap comes in - it’s a hash table that maps the data field names you chose to these letters that are used in the Grid’s data table. Finally, we substring that sucka because the first character returned by fieldMap is always an ‘@’ sign. In this line of code, I’m extracting the ‘ID’ field from the data table, for the selected row that I am processing.IDs.push(ID): Finally, we push the extracted ID onto the IDs array.
- Last but not least, we create the XHR object that allows us to communicate with our server-side method:
var xhr = new nitobi.ajax.HttpRequest(): We create the XHR object with this bit of code - it’s a function in the Nitobi Toolkit that will create a cross-browser XHR object. Yes, the XHR is implemented differently in different browsers. *cough* Internet Explorer *cough*xhr.handler = "delete.ashx?ids=" + IDs.join(","): The handler is the URL to the server-side method that will do the actual delete operations for us. In this case, the handler is called ‘delete.ashx’ (an ASP.NET page), and we tack on a querystring parameter ‘ids’ with the joined row IDs that we accumulated in step 5.xhr.async = true: This sets the mode of the XHR - we want the XHR to be sent and processed asynchronously, so we don’t hang the browser while our server method is executing.xhr.completeCallback = function(evtArgs) {eval('var message='+evtArgs.response+';');alert(message.status);}: An optional step, here we assign a JavaScript function that executes once the XHR object receives a response from the server. In this example, my ‘delete.ashx’ script writes out some JSON as a response. One of the members of this JSON object is called ’status’. Basically, once we receive a response from our server script, an alert is shown with the status message sent from the server method.xhr.get(): The last step, the get() function fires off the XHR.
Server-Side Method
I can’t really help you on this one, totally depends on what platform you are running your website on, what server-side language you’re using, etc. However, what I can tell you is what I did. As you can see, the ‘delete.ashx’ script is called to do the deletes. At a high level, the script processes the querystring and extracts the IDs of the particular items that the user selected in the Grid. In this example, the Grid’s get handler housed the unique database IDs for the items in the Grid in the ‘ID’ field (remember that fieldMap magic?). I send these IDs to the delete script, which parses them and calls a database DELETE operation on database rows with the specified IDs. Simple, no?
That’s it for this week, hope some of you folks out there found it useful, or at least insightful. Next week I’ll talk about using the ImageEditor column type available to sexy up the Grid.