California Ajax Solutions Team

www.CalAST.com

The SelectorTable Object

Kenneth F. Greenberg

In this article, I will describe a useful JavaScript object for presenting a site user with the results of a query in tabular form, and then allowing that user to choose one item for further action. It's easier to understand the functionality with a real-world example, so I'll provide one from an area in which I have a significant interest - the wine cellar.

Suppose I have a database representing the contents of my wine cellar on a server somewhere, and I want to design an "Ajaxian" web site for retrieving information from that database. The structure of the database is irrelevant to this example, but let's assume there is some way choose a wine by any of a number of common attributes -- vintage, varietal, vintner, region, and so on. (No, you don't need to understand anything about wine to follow this example, but it probably helps.) So now I wish to query the database about all wines that match some set of criteria.

I've had a rough day in the data center, and decide I need some red meat to help me recover. So I'm in the mood for a Napa Valley Cabernet Sauvignon. I have some mechanism for filling in a form to specify I am interested in the region "Napa Valley" and the varietal "Cabernet Sauvignon." When I submit that form, it turns into a database query, and the database returns a list of matching wines that are the results of my query. Probably, they came back in XML or JSON, but it's not too important. It's what happens next that is the subject of this discussion.

I can now pick one of the wines on the list (assuming there were multiple records that matched) and do "something" with it. One obvious choice would be to just choose one that sounded good and remove it from the database, since it won't be around once I drink it. Another might be to show more details to help me decide. I want to make this as easy as possible for me (since I've had a bad day) so presentation of the query results should be straightforward, choosing one should be trivial, and performing an action on the one item I have chosen should be obvious.

I do this sort of activity -- present query results, choose one, do something with it -- on web sites so often that I decided I needed some very reusable code to make this aspect of the design less time-consuming. As a great believer in object-oriented programming, I came up with the SelectorTable object. I will tell you more about the object and how it's used, but it probably helps to look at one first.

As you can see, this is a table (contained in a form) with a left column consisting of radio buttons. The contents of the rest of the table are completely application dependent, so the object lets you specify the headers and add rows dynamically at run time as you work your way through the query results. There's really not much to the form except the table itself and one or more buttons to move to the next step, once one item from the table has been selected. The buttons are not part of the SelectorTable object, but share the same form with it. You only add buttons if you need to perform further work on the selected object. This is usually the case, but the buttons might be in a separate navigation bar if this better fits the style requirements of the page.

Code Example

So, the next thing you probably want to see is what the code for this looks like. As with most objects, the bulk of the work is done in the methods themselves, so the referencing application is pretty simple. I'm using a different example than the one shown above, but the idea is the same.

var wTable = new SelectorTable('wines');
wTable.addHeader('Varietal','Year','Vintner');
var form = document.createElement('form');
form.setAttribute('id','chooseForm');
wTable.appendTo(form);

What we've done here is create the table itself, give it a name, teach it the names of the column headers, and create an enclosing form to which we attach the table. If we are going to validate the form before sending it to a server script or just manipulate it with JavaScript in some other way, it's useful to give it an id, so we've done that too. But you don't have to really use a form for this - you could just attach it to a DIV or some other element.

Of course, the obvious thing we have not yet done is put any meaningful data in the table. How that happens is somewhat dependent on how the data is returned by your application. Let's assume you are using Ajax and the results come back in XML. So your XmlHttpRequest is going to return a document, which you can then examine and add rows to the table based on what the database sent you. Perhaps your results look like this:

<winelist>
 <wine>
  <vintage>1994</vintage>
  <vintner>BV</vintner>
  <varietal>Cabernet Sauvignon</varietal>
 </wine>
</winelist>

Of course, there will be more wines in the list, each enclosed in a wine element. Now we just need to get these things into the table. I'm going to make the assumption that you first checked for a zero-length list, or else there was really little point in even creating the table.

var wineList = wineObj.getElementsByTagName('wine');
if (wineList.length == 0) {
 document.getElementById('scratchpad').innerHTML = "Buy more wine";
} else {
 // create the table as shown above
 vintages = wineObject.getElementsByTagName('vintage');
 vintners = wineObject.getElementsByTagName('vintner');
 varietals = wineObject.getElementsByTagName('varietal');
 for (i=0; i<wineList.length; i++) {
  varietal = varietals[i].firstChild.nodeValue;
  vintage = vintages[i].firstChild.nodeValue;
  vintner = vintners[i].firstChild.nodeValue;
  wTable.addRow(varietal,vintage,vintner);
 }
}

In this section, we walk the list of wines returned from our query and extract the vintage, vintner, and varietal information. When we have retrieved what we need, we call the addRow method of the SelectorTable object to add the information to the table. Each row gets a radio button, of course, but that all happens inside the SelectorTable object, so you don't need to worry about it.

You have seen several of the methods of the SelectorTable, but there is one more method for building the table that you might find useful. Sometimes, the selected item will later be returned to the server as part of a POST operation, since the server must process database updates. If the returned table rows correspond to rows in an actual database table with a primary key, you might want to always return that information with the results for the original query. This simplifies telling the server exactly which record to modify later on. But the user probably doesn't ever need to see this, and in fact it probably hurts clarity.

A good solution to this problem is to simply assign the primary key value to the radio button itself. That way, it is never displayed but it remains associated with the rest of the row information. We provide an alternate form of the addRow method called addRowWithValue. In this method, the first argument is not a table column value but instead is a value that gets assigned to the radio button itself. So if the XML returns the wine ID used as the primary key as "id", you simply change the call to:

wTable.addRowWithValue(id,varietal,vintage,vintner);

You still see the same three columns and the radio button, but the value of id has now been attached to the radio button. Only the other three values appear in the display.

Now you know how to create a SelectorTable object and use its methods to add information to it, but how do you manipulate it further in JavaScript? Looking at some code will probably help. This will illustrate one of the trickier issues we need to deal with -- an array with one element may not look like an array in an event handler. Since we can't predict how many matches will be found in the database, we have to deal with the special cases of zero, one, or many results. We've already handled the "no matches" case by not even bothering to display the table, but the one result case can be handled in a browser-agnostic way in the event handler itself. And since the object "knows" the number of rows, you can simply retrieve this property.

Of course, you could just send the form over to the server and fix everything there, but it's generally best practice to validate data before sending it. For example, if the user didn't pick anything from the table, it's not meaningful to ask the server to do something with it. And, since good Ajax design encourages minimizing data transfers, you might find that all you really need to send is that ID you stashed away as the radio button value. But to do that, you need to know which button was selected. Here's an example of how this can be done.

var which = wTable.getSelected();
if (which == null) {
  alert('You must choose one item');
  return;
}

Here we take advantage of the getSelected method, which can tell us which button was pressed. If no button was selected, it returns null to indicate that the user didn't choose a line yet. We do allow the user to avoid making a choice if there is only one item in the table; this is simply a design choice. If there are less than two lines in the table (not counting the header), then getSelected always returns 0, indicating the first line. If you want to force the user to actually click on the button even when there only is one, you can modify the code to do so.

If the hidden button value is the item of interest to us, we can retrieve it using the getSelectedValue method instead of getSelected.

Paging

One other feature of this object that is quite useful is that the table can be paged for display purposes. If the database query returns 125 rows, your users probably will be unhappy to have to scroll through that many lines in a table. But the SelectorTable allows you to specify the number of lines you want to see at one time (the page size) and provides a method for turning paging on. This causes a series of hyperlinks to be displayed under the table, and only the first page (as determined by how many lines per page you specified) to be shown. You can then walk through the pages of data using the hyperlinks. The values of the rows and selectors are unchanged, since all the rows are still in the table; they just don't all show up on the screen at the same time. A table with three pages would display Prev 1 2 3 Next underneath the table. The Prev and Next links move backwards and forwards through the pages, and the numbered links take you directly to that page.

Use of this feature requires a small amount of work on the designers part, since the elements in the DOM tree don't know about the JavaScript objects. Thus, you can tell which link was clicked on, but not which SelectorTable object it belongs to. So you simply have to provide an event handler for the buttons which will call an appropriate paging method in the object. It's a very small function (other than dealing with browser issues), as shown below. The IDs passed to the event handler indicate which hyperlink the user clicked on, and reflect the name of the table (blt in the example below). The SelectorTable class provides three methods for moving through the pages.

var blTable;

function pageEventHandler(e) {
  // This section is all about being browser independent
  var targ;
  var ev = e;
  if (!ev) { ev = window.event; } // in case it is IE
  if (ev.target) {
    targ = ev.target;
  } else if (ev.srcElement) { targ = ev.srcElement; }
  if (targ.nodeType == 3) // defeat Safari bug
  {
    targ = targ.parentNode;
  }
  // actual work starts here
  if (targ.id == 'bltNext') {
    blTable.nextPage();
  } else if (targ.id == 'bltPrev') {
    blTable.prevPage();
  } else {
    blTable.showPage(targ.id.substr(3));
  }
}

function showTable() {
    blTable = new SelectorTable('blt');
    blTable.setPageSize(10);
    blTable.enablePaging(pageEventHandler);
    ...
}

Here, blTable is the SelectorTable object itself, which was created with the tag "blt" as its argument. This forces the buttons to be called bltPrev and bltNext, or the hyperlinks to be called bltPrev, blt1, blt2, and bltNext.

API

At this point, we should just look at the API (Application Programming Interface) for the selector table object, which will tell you much of what you need to know about how to use one. This object changes frequently, as it gets used in more sites, so expect the API to be somewhat dynamic.

While this table provides more descriptive text, there is also a jsdoc3 version based on the actual contents of the file. Many people may wish to look at that one, as well. Since the jsdoc version is produced from the source, it may contain methods that have not yet been documented here.

Method Arguments Returns Purpose
SelectorTable tag (string) - the id of the object new object This is the constructor function for a SelectorTable object. It creates a new SelectorTable, assigns it the specified tag name, and creates table and tbody elements for it.
appendTo parent element n/a Adds the table as a child of the specified DOM element (often a DIV).
onClick listener (function) n/a This method is used to add an event listener to the selector radio buttons. Since the same listener is applied to all buttons, this must be invoked after all the data rows have been added to the table.
addHeader array of strings n/a This method creates columns in the table (and their associated TH elements) for each string in the array.
addRow array of strings n/a This method adds a new row to the table, with column data created from the specified strings.
addRowWithValue value, array of strings n/a Adds a new row of data to the table as above, but uses the first argument as the value of the radio button.
getRowCount n/a integer This method returns the number of rows in the table, not counting the header.
getSelected n/a integer This method returns the index of the selected row, as determined by the radio button having been clicked on by the user. If no rows have ever been added to the table, this method return null. For a single data row table, a value of 0 is always returned; the user does not need to actually click the button since the selection is obvious. For a table with two or more data rows, then the row index is returned if one of the buttons has been selected. If no button has been selected on a multi-row table, null is returned.
getSelectedValue n/a value This method returns the value of the button clicked by the user. It is intended for use with tables constructed by the addRowWithValue method. If no rows have ever been added to the table, this method return null. For a single data row table, the value of the only button is always returned; the user does not need to actually click the button since the selection is obvious. For a table with two or more radio buttons, then the value of the button clicked by the user is returned. If no button has been selected on a multi-row table, null is returned.
getSelectedColumn column index column value This method returns the contents of the specified column for the row currently selected by the user. The usual rules about tables with no selected rows apply.
hideUnselected n/a n/a Collapses the table display so only the selected row is visible.
makeSortable n/a n/a Makes it possible to sort the table by clicking on a column header.
updateRowWithValue value, array of strings n/a This method allows replacing a table row's content, possibly by editing or the effects of some other action. The first argument is used to find the row in the table, so this can not be changed. The remaining arguments replace the current values.
getRowValue row number value This method returns the value of the specified row.
setColors color1, color2 none This allows the designer to specify the background colors of each row. Color1 is used for the table generally; color2 is used for the selected row. This makes the selected row stand out more - and makes it obvious when no row is selected..
setPageSize sz (number) n/a This method specifies the number of lines (not counting the header) that represent a page on this table for display purposes.
showUnselected n/a n/a Undoes the effect of hideUnselected method by making all rows visible.
enablePaging listener (function) n/a This method enables the paging mechanism, causing navigation controls to be shown below the table when it gets large enough. The specified event listener will be called when the user clicks on one of the buttons.
prevPage none n/a If not already showing the first page, display the previous page of the table. Called from the event listener.
nextPage none n/a If not already showing the last page, display the next page of the table. Called from the event listener.
showPage page number n/a Show the specified page.

Implementation and Notes

The source code for the SelectorTable object may be found here.

I like to think the implementation is fairly straightforward, but clearly this reflects some personal programming style. This was originally implemented with inner functions. There are not usually many tables in a web application, so efficiency is less of an issue here than if you were creating hundreds of objects of this type. However, in terms of enforcing some best-practice discipline, I rewrote it using prototypes.

Styling may be applied in any way that fits your web site design. A SelectorTable object has the CSS class SelectorTable as the class attribute of the table.

This object does illustrate how the JavaScript designer's life is not an easy one. There is code included here to work round a peculiar IE (Internet Explorer) behavior, which I will now describe. In theory, it ought to be possible in DOM manipulation code to simply create objects dynamically, set their attributes, and add them to the web page structure where needed. They should then work exactly the same as if they were created statically in the HTML itself. Unfortunately, there is a well-known problem with radio buttons and IE where this doesn't work. If you write:

var input = document.createElement('input');
input.setAttribute('type','radio');
input.setAttribute('name','radioButtonGroup');

you would expect these radio buttons to work normally. They won't. When you click on one, it won't be selected in IE, although they will work fine in EOMB (Every Other Modern Browser). Clearly, this is exactly the way the DOM is supposed to be used, according to virtually every reference.

However, Microsoft has stated that for dynamically allocated elements, you can not add the name after the element has been created. This is pretty much in violation of W3C standards, but so it goes. In fact, you almost always can do this, even in IE, but not for radio buttons. The solution recommended by Microsoft is to code something like:

document.createElement('<input type="radio" name="radioButtonGroup">');

but of course this only works in IE and not anywhere else - it violates W3C standards. Browser detection is generally considered a symptom of low moral values, but feature detection doesn't seem to help here. In fact, if you set the name and read it back in IE, you will get the name just as you set it. But the radio buttons just won't work. The standard workaround, and the one I used in the SelectorTable object, is to drop back to the innerHTML approach. This is browser-agnostic, and the code is simple. I have found that IE is somewhat resistant to assigning innerHTML to any element, and that's probably bad practice anyway. (It is not legal to assign innerHTML to elements that don't have an "inner" component, like <br>.) But IE is happy to let you assign innerHTML to a SPAN. So the resulting code is:

var input = document.createElement('span');
input.innerHTML = '<input type="radio" name="radioButtonGroup">';

The actual code is slightly more complex, since the name of the group is formed from the name assigned to the SelectorTable object when it was created, but you get the idea here. This is generally a reliable approach to this problem.