Skip to Navigation | Skip to Content



Archive for the 'Complete UI' Category

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.

  1. 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.
  2. 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.
  3. 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.
  4. The next conditional will just bring up a confirmation dialog asking the user if he or she really wants to delete the selected items.
  5. The for loop here is where the Complete UI magic happens. The loop iterates over the selected rows, and does this:
    1. 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…
    2. 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.
    3. IDs.push(ID): Finally, we push the extracted ID onto the IDs array.
  6. Last but not least, we create the XHR object that allows us to communicate with our server-side method:
    1. 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*
    2. 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.
    3. 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.
    4. 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.
    5. 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.

Posted in Complete UI | 2 Comments » | Add to Delicious | Digg It

Look What Complete UI’s Grid Can Do, part one | March 26th, 2009

On a recent Nitobi web development project, we were tasked with improving a social network-ish site’s management and administration pages. Up to that point in the project, the administration panels, as I call them, were fairly neglected and had a couple of different problems:

  • They were slow. As the site was gaining popularity and grew in size, the content that the site housed needed to be managed, maintained, edited, deleted, etc. more and more often. There was also more of this content to manage. These two things compounded together to start slowing the administration pages down.
  • There was little to no design effort put into how the administration panels worked. This resulted in the workflow of the panels to be very cludgy.

First, I enlisted the help of Nitobi’s lead UX Designer, Chris, to help come up with some good designs for the administration panels. We looked at what these management pages provide and how our administrators use them to guide the design, and the end-result is a night-and-day improvement on the previous pages (worth a blog post in itself).

Before

Here is a thumbnail of the administration page before (click for bigger):

After

Here is a thumbnail of the page after we were done with it (again, click for bigger):

The new page is much more concise, with a nicer design and much more responsive user interaction. The grid only summarizes information in each of its rows - each element actually has a lot more content to manage than what is shown in the grid. The old administration page would show this extra content at the bottom of the page (as depicted in the screenshot), which made the page quite overwhelming. In the new page, I removed this section and made a JavaScript and DHTML-based edit ‘dialog’ that housed this extra information, cleaning up the layout and vastly improving the user experience.

Anyways, the point of this post is to show off how I used Nitobi’s own Complete UI component, Grid, in this redesign, and show off some little extra bits that I implemented to improve the user experience that much more. For those that don’t know about it, Complete UI is a sweet suite of open-source web components that you can plug into almost any web page. Grid, one of the components in this suite, is quite extensive, with a lot of little features that make it a powerful tool to be able to plug into any ol’ web page. I’m going to be getting into the details of this component, so it might be useful to have the Complete UI HTML Declaration Reference and Nitobi JavaScript APIs handy. I’ll break up the work done with the Grid over a couple of blog posts, as it is a lot of content. Today, I’ll talk about setting up your own pretty pagination controls.

Custom Paging Controls

One of the reasons the old administration pages were slow is because each page would load all relevant data right away. In today’s AJAX world, this is unnecessary and wasteful. Simple pagination would definitely help with this - why not just load the data we need, as we need it? “But wait a second, here, Filip,” you may be saying to yourself, “the Complete UI Grid already has paging! Why implement it again?” Not that I have anything against the built-in pagination in Grid, but I have something against the built-in pagination in Grid ;) Chris and I wanted the pagination controls to be big and salient and obvious, so he designed some and I implemented them. It was actually quite easy, a little HTML, a couple JavaScript functions and Grid. I’ve set up an archive that you folks out there can download, it contains files and source code necessary to get a skeleton of this example up and running.

  1. First, we disable the built-in button and pagination toolbar in the Grid. You can do this declaratively by specifying the toolbarenabled=”false” attribute of the <ntb:grid> tag.
  2. Here is the small HTML declaration for my custom paging controls that I added to my page:
    <div id="pager_control" class="inline">
    <div id="pager_first" class="inline FirstPage" onclick="Pager.First('myGrid');" onmouseover="this.className+=' FirstPageHover';" onmouseout="this.className='inline FirstPage';" style="margin:0;border-right:1px solid #B1BAC2;"></div>
    <div id="pager_prev" class="inline PreviousPage" onclick="Pager.Previous('myGrid');" onmouseover="this.className+=' PreviousPageHover';" onmouseout="this.className='inline PreviousPage';" style="margin:0;"></div>
    <div class="inline" style="height:22px;top:3px;">
    <span class="inline">Page</span>
    <span class="inline" id="pager_current">0</span>
    <span class="inline">of</span>
    <span class="inline" id="pager_total">0</span>
    </div>
    <div id="pager_next" class="inline NextPage" onclick="Pager.Next('myGrid');" onmouseover="this.className+=' NextPageHover';" onmouseout="this.className='inline NextPage';" style="margin:0;border-right:1px solid #B1BAC2;"></div>
    <div id="pager_last" class="inline LastPage" onclick="Pager.Last('myGrid');" onmouseover="this.className+=' LastPageHover';" onmouseout="this.className='inline LastPage';" style="margin:0;"></div>
    </div>

    In a nutshell, it has a couple of spans that house the current and total page numbers, and the first/last/next/previous buttons (styles defined via the classes) that call methods in a JavaScript Pager object (more on that next).
  3. Next, we have to somehow tell our server-side data handler to grab only a particular page of data. It doesn’t really matter what language you write your server-side scripts in. All we have to do is pass paging parameters to our server methods via the querystring, and then make sure our server-side methods parse these parameters and use them to retrieve the proper page of data. This is all controlled via the JavaScript Pager object I was talking about earlier, and here is the JS:

    var Pager = {
    PageSize:12,
    CurrentPage:0,
    TotalPages:0,
    TotalRows:0,
    Last:function(GridID) {
    Pager.CurrentPage = Pager.TotalPages;
    Pager.BindToGrid(GridID);
    },
    First:function(GridID) {
    Pager.CurrentPage = 1;
    Pager.BindToGrid(GridID);
    },
    Previous:function(GridID) {
    if (Pager.CurrentPage > 1) {
    Pager.CurrentPage--;
    } else {
    Pager.CurrentPage = Pager.TotalPages;
    }
    Pager.BindToGrid(GridID);
    },
    Next:function(GridID) {
    if (Pager.CurrentPage < Pager.TotalPages) {
    Pager.CurrentPage++;
    } else {
    Pager.CurrentPage = 1;
    }
    Pager.BindToGrid(GridID);
    },
    Reset:function(GridID) {
    Pager.CurrentPage = 0;
    Pager.TotalPages = 0;
    Pager.BindToGrid(GridID);
    },
    // Uses the paging variables in the Pager object to re-bind the Grid with the right page.
    BindToGrid:function(GridID) {
    g = nitobi.getComponent(GridID);
    if (Pager.TotalPages == 0) {
    g.datatable.setGetHandlerParameter('StartRecord',0);
    } else {
    g.datatable.setGetHandlerParameter('StartRecord',(Pager.CurrentPage-1)*Pager.PageSize);
    }
    g.loadingScreen.show();
    g.dataBind();
    }}

    All of the Pager functions take the Grid ID as a parameter, for easy re-use with other Grids, if one so desires. What happens when you call Next(), Previous() or any other function there is the querystring parameters for paging get set in the Grid’s datatable - this is the component that houses the Grid’s data and handles data retrieval from and communication with the data handler server methods (via XHRs). Potentially, you could also pass the pagination size as a parameter, if you wish to dynamically vary it too. Just remember that whatever parameters you set using the Grid’s setGetHandlerParameter (click here for more info - scroll down to the getDatasource() method to see a small example) need to be parsed by your data handler script, otherwise it won’t be used! In my example above, I set a parameter called ‘StartRecord’, which I parse in my data handler and then use in my data retrieval query. Then simply call dataBind() on the Grid to force Grid to re-connect to the handler and retrieve the data.
  4. Finally, we need to update the Pager object with the right number of records retrieved from the database. I did this by assigning an error attribute to the root XML node of the data handler’s response, which would contain the number of rows the server processed, and then hooked in a JavaScript function to the Grid’s OnDataReadyEvent (which gets fired every time a data request from the server is complete) to parse this number back into the Pager object. There are other, probably more elegant ways to implement this part, but it seemed like the quickest way for me, and I didn’t use the error attribute for anything else, so why not? So, first I hooked in a function to the event via the HTML declaration: ondatareadyevent=”HandleReady(eventArgs);”. This is what the function looks like:

    function HandleReady(eventArgs) {
    var g = eventArgs.source;
    Pager.TotalRows = parseInt(g.datatable.handlerError);
    Pager.TotalPages = Math.ceil(Pager.TotalRows/Pager.PageSize);
    $('pager_total').innerHTML = Pager.TotalPages;
    if (Pager.TotalPages > 0) {
    if (Pager.CurrentPage == 0) {
    Pager.CurrentPage = 1;
    }
    } else {
    Pager.CurrentPage = 0;
    }
    $('pager_current').innerHTML = Pager.CurrentPage;
    // Hide Grid loading screen.
    g.loadingScreen.hide();
    }

    The error attribute that we set in the server response is easily accessible via the datatable.handlerError member, as demonstrated in the above function. Once we parse the total rows, we just set the span contents that show off the current and total pages, hide the Grid loading screen, and we’re done!

That’s it for this week! Next week, I’ll post about improving the editing experience in Grid with the help of JavaScript, the ImageEditor column available in Grid, and doing your ‘own thang’ by manipulating the Grid’s DOM nodes.

EDIT: I’ve added before/after pictures of our work, as well as a downloadable example page of the Complete UI Grid with a custom paging control, available -> here <-. Please read the attached readme file to get the example running properly - enjoy!

Posted in Complete UI | 4 Comments » | Add to Delicious | Digg It


Search Posts

You are currently browsing the archives for the Complete UI category.

Archives

Categories