Monday, August 25, 2008

Totaling Table Columns Deconstructed

So the last post provided a function that calculates totals based on classes placed on rows in a table. This post will go through that function line-by-line so all its secrets are revealed. If you're already a jQuery pro, you can skip these "Deconstructed" posts, as they're really aimed at a detailed understanding of the code.

Now, before we even start, there's a piece of code that's not mentioned. It's the call to the jQuery library, without which all is for naught. It should look like this:

<script type="text/javascript" src="../js/jquery.js"></script>

The part that will change on your local file is the src attribute. The value there needs to be whatever the path is to your local jQuery library. With that in place, we're ready to move on. Let's get to it.

function totalTableSubRows(tableID, totalClass, dataClass, columnNumber) {

The first line of the function is its signature. It specifies that the function takes four arguments. They are:
tableID: the HTML id of the table to use
totalClass: the CSS class of the row where totals should be placed
dataClass: the CSS class of rows where data is located
columnNumber: the column number (left to right, starting at 1) where the data is located

Now we get into the meat of the thing. First, some variables are set:

tableSelector = "#" + tableID;
totalSelector = "." + totalClass;
dataSelector = "." + dataClass;
columnIndex = columnNumber - 1;

The first three assignments take the arguments passed into the function and turn them into the selector strings we're going to need in order to work with jQuery. The hash (#) is the symbol for matching an HTML id attribute, so it gets prepended to the tableID. The dot (.) is the symbol for matching an HTML class attribute, so it gets prepended to the class names for the totals row and the data rows.

The fourth assignment adjusts the column number so that it uses JavaScript-friendly zero-based indexing, instead of the more human-friendly 1-based indexing. That is, when we count the columns in the table, we naturally start with 1 at the leftmost column, and count to the right. So we would say the second column's index is 2. But JavaScript calls the first thing 0, so the second column would be 1, and so forth. Subtracting 1 from the argument allows us to use human-friendly counting, while the function uses JavaScript-friendly counting. Everybody's happy.

$(tableSelector + ' tr' + totalSelector).each(function(){

Now starts the jQuery. The $ symbol is an abbreviation for "the jQuery object". So $() is the same as saying jQuery(). We'll be seeing this a lot.

jQuery's argument is called a selector. The selector causes jQuery to return a JavaScript object of type jQuery which contains every element in the DOM that matches the selector. jQuery methods can then act on the members of this return set.

The first selector combines the tableSelector and totalSelector variables, separated by space-tr-space. Since the tableSelector specifies the table's ID, and the totalSelector specifies the class of the totals row(s), this combination says, "select all td elements with a class equal to the value of totalSelector that are descendants of the element whose ID is equal to the value of tableSelector".

The .each() method says "do what's inside my parens for each member of the selected jQuery object." So what's inside its parens? Turns out, a good bit.

In JavaScript, "function" is a first-class datatype. That just means that we can specify a function as a set of instrutions that can then be passed around assigned by its name. If we don't need to use a name, we can use the so-called "anonymous function" construct, as seen here. Notice there's no function name after the keyword function. We're just creating a function on the fly and handing it as an argument to the each() method. By doing this, we've assigned behavior that will be performed for each member of the return set (in this case, all the total rows).

$(this).children("td:eq(" + columnIndex + ")").html(0);

Now we have another jQuery selector. This one is based on the this keyword. Note that it is not within quotes. To jQuery, this means "the element referred to by each() at this time". In this case it means "this total row".

The children() method means "look for the selector in my argument in the elements that are direct children of the return set". At this point, the return set is a single row of the table (specified by $(this)). The selector passed to it uses the :eq() filter to find a specific td element. In this case, it's finding the cell specified by the columnIndex variable.

The next part of the line shows an example of jQuery chaining. Every jQuery method returns the same return set for which it is a method. This means that methods can be strung together in a chain, with each method operating in turn on the return set. This is the case with the html() method at the end of this line. It's simply setting the HTML inside the selected td element to 0.

So this line is simply saying "Set all the td's in the specified column of each total row to 0."

total = 0;

Now we initialize the total variable to zero.

$(this).nextAll().each(function(){

And use another jQuery selector. Again, we're using this to operate on each total row (we're still inside the function for the each() method that selected total rows, remember). The nextAll() method says "select all sibling elements that come after the specified element". And each() once again allows us to assign behavior to each of those siblings.

if($(this).hasClass(dataClass)){

Now some conditional logic: if the selected element (row, in this case) has the value specified by dataClass as its class attribute, we'll do the next four lines.

thisVal = $(this).children("td:eq(" + columnIndex + ")").html() * 1;
if (!isNaN(thisVal)) {
total += thisVal;
}

The first line pulls the value out of the specified cell (td) and makes sure JavaScript will treat the number as a value by multiplying it by 1. If that multiplication doesn't result in "NaN" (i.e. the cell contained a number), that number is added to the total.

If the row doesn't have the dataClass assigned as its class, then we'll do the next rows:

} else {
if($(this).hasClass(totalClass)){
return false;
}
}

We don't want to do anything, unless run across a row that has the class specified in the totalClass variable. In that case, we want to quit adding values to the total, because we're done with the children of the current total row. This is what the return false statement does.

And this is the end of the function definition inside the each() after the nextAll():

});


$(this).children("td:eq(" + columnIndex + ")").html(total);

Now we're back in the each() function of the original selector (the total row), so we just select the child td element in the desired column (:eq() with the columnIndex variable) and assign the calculated total as its HTML content (html()).

This is a good place to point out the dual nature of many jQuery methods. These methods at as both "getters" and "setters". If no argument is given, the method returns the requested value. If an argument is specified, the method sets the specified value. So, in this example, we've used the html() function to both set the value and fetch the value of some td elements.

});
}

Finally, we're just closing out the function that's inside the each() method, the each() method itself, and the totalTableSubRows function.

To call this function, we just need to specify the desired arguments. As we saw in the previous post, those calls look like this:

totalTableSubRows("myTable","regionRow", "storeRow",2);
totalTableSubRows("myTable","totalsRow", "storeRow",2);

The first one says, "Put the totals of storeRows' second columns in their respective regionRows".
The next one says,
"Put the totals of storeRows' second columns in their respective totalsRows".

Happy jQuerying!

2 comments:

Dan Dascalescu said...

Would be great if you could hyperlink "last post" to http://jquerynab.blogspot.com/2008/08/adding-table-columns.html

Thanks!

Sayan Guharoy said...

you can also check my simple implementation as a starter excel like jquery