The following table contains an ongoing log of hikes that are planned, and actual miles hiked. The idea here is that we want tto be able to just enter new data in the table, and have the bottom row update automatically. We want the totalling function to be robust enough to handle blank cells (e.g. the ones in the table below that we "haven't filled in yet".)
Locations | Date | Miles (planned) | Miles (actual) | Comment |
---|---|---|---|---|
Alapocas Woods | 02/18/06 | 1 | 1 | busy that day; only time for a short hike |
Brandywine State Park | 02/25/06 | 5 | 0 | too lazy to get off of couch |
Judge Morris Estate | 03/04/06 | 10 | ||
Middle Run | 03/11/06 | 7 | ||
Totals |
We'll proceed step-by-step:
In this step, we just add a stub function that will run as soon as the page loads. In later steps, we'll fill in that function with the code to do our work.
We start by adding a element <script type="text/javascript"></script>
inside the <head>
element.
<script type="text/javascript"> </script>
We add a global variable debugScript = true;
so that we can turn debugging on and off. For now, we turn it on.
(You may be surprised to see Phill Conrad advocate the use of a global variable, when you know how much he preaches against them in CISC105 and CISC181. Well, let's not be fundamentalists about our coding—there are indeed times and places for global variables. They are rare, but this is one of them.)
<script type="text/javascript"> var debugScript = true; </script>
We also add a special mix of XHTML and JavaScript comment brackets, to hide the JavaScript from XHTML parsers. You'll see this pattern in lots of JavaScript pages. Some sources are now advising using CDATA sections instead, which theoretically is the "proper" way to handle this. However, I've run into trouble with the CDATA method on some browsers. Probably at some point in the future CDATA sections will become the best way to do this, but for now, the comment brackets seems the best way to go.
<script type="text/javascript"> <!-- var debugScript = true; // --> </script>
Add a simple stub function called finishTable()
that, for the moment, just prints an alert box telling us that the function is running—but only when debugScript is true.
<script type="text/javascript"> <!-- var debugScript = true; function finishTable() { if (debugScript) window.alert("Beginning of function finishTable"); return; } // --> </script>
Add an onload
event handler on the <body>
tag to call our script when the page loads:
<body onload="finishTable();">
In this step, we just show how to locate the data in the table that we want to find the sum of. Here's how we proceed:
In order to be consistent with how the Document Object Model (DOM) treats tables, be sure we have a <tbody>
element inside our <table>
element, and that the <tr>
elements we are looking for are inside that <tbody>
element. This <tbody>
element doesn't seem to be that important from the standpoint of making the table display correctly or validates as XHTML 1.1, but the DOM seems to want it to be there.
<table border="10" cellspacing="2" cellpadding="1">
<caption>
Hikes
</caption> <tbody>
<tr>
<th scope="col">Locations</th>
<th scope="col"> Date </th>
<th scope="col">Miles (planned)</th>
<th scope="col">Miles (actual)</th>
<th scope="col">Comment</th>
</tr>
<tr>
<td>Alapocas Woods </td>
<td>02/18/06</td>
<td>1</td>
<td>1</td>
<td>busy that day; only time for a short hike </td>
</tr> ...
</tbody> </table>
Then, make sure the page validates as valid XHTML 1.1. (You won't have much success using DOM manipulations if you have invalid XHTML in the first place—being able to use DOM manipulations is one of the main motivations for writing strictly to the XHTML standard!)
Now add a line of JavaScript inside our finishTable() function to find the table we are interested in. There are several ways to do this, but the easiest is to put an id attribute on the table open tag, and use getElementById:
// this next line goes inside the finishTable() function var tableElem = window.document.getElementById("hikeTable"); ... <!-- this next line goes inside the body of the page --> <table id="hikeTable" border="10" cellspacing="2" cellpadding="1">
Put the line that finds the table element into try/catch block. This way, if we make a mistake, we can find it more easily.
As an example of a mistake we might make, consider what would happen if we wrote <table id="hiketable" ... >
in our XHTML and then then did a getElementById("hikeTable");
in our JavaScript code—note the uppercase vs. lowercase. Try that once to see how the try/catch block below will help us find the error.
function finishTable() { if (debugScript) window.alert("Beginning of function finishTable"); try { var tableElem = window.document.getElementById("hikeTable"); } catch (ex) { window.alert("Error in finishTable()\n" + ex); } return; }
One important thing to note is that JavaScript does not have block scope. That is, the variable tableElem
, declared
inside the try
block above is known throughout the entire function, not only inside the braces that delimit the try block. This
is a crucial difference between JavaScript, and what you may be used to from C, C++, and/or Java.
For more about this important aspect of scoping in JavaScript, see the CISC474 reading notes for Section 4.9.2 in Sebesta.
Traverse all the rows in the table (except the top and bottom rows). To do this, we first get a reference to the tbody element, and then determine the number of rows in the table. We can iterate over these rows with a simple for loop. The result should be a series of alert boxes showing us the values in the "Miles (planned)" column of the table, which is column 2—note that column numbers start from 0, not from 1).
var tableBody = tableElem.getElementsByTagName("tbody").item(0);
var i;
var whichColumn = 2; // which column in the table has what we want
var howManyRows = tableBody.rows.length;
for (i=1; i<(howManyRows-1); i++) // skip first and last row (hence i=1, and howManyRows-1)
{
var thisTrElem = tableBody.rows[i];
var thisTdElem = thisTrElem.cells[whichColumn];
var thisTextNode = thisTdElem.childNodes.item(0);
if (debugScript)
{
window.alert("text is " + thisTextNode.data);
} // end if
} // end for
A tutorial from the Mozilla Developer Center titled Traversing an HTML table with JavaScript and DOM Interfaces may be helpful for understanding the steps in this section in more detail.
Now that we've shown that we can find the table cells that we are interested in, we now show how to add them up. In this step, we also refactor the code so that we can reuse our solution to find the sum of any column in the table.
We assume the reader is, by now, getting the hang of how JavaScript works, so we show only the finished solution from this step.
<script type="text/javascript"> var debugScript = true; function computeTableColumnTotal(tableId, colNumber) { // find the table with id attribute tableId // return the total of the numerical elements in column colNumber // skip the top row (headers) and bottom row (where the total will go) var result = 0; try { var tableElem = window.document.getElementById(tableId); var tableBody = tableElem.getElementsByTagName("tbody").item(0); var i; var howManyRows = tableBody.rows.length; for (i=1; i<(howManyRows-1); i++) // skip first and last row (hence i=1, and howManyRows-1) { var thisTrElem = tableBody.rows[i]; var thisTdElem = thisTrElem.cells[colNumber]; var thisTextNode = thisTdElem.childNodes.item(0); if (debugScript) { window.alert("text is " + thisTextNode.data); } // end if // try to convert text to numeric var thisNumber = parseFloat(thisTextNode.data); // if you didn't get back the value NaN (i.e. not a number), add into result if (!isNaN(thisNumber)) result += thisNumber; } // end for } // end try catch (ex) { window.alert("Exception in function computeTableColumnTotal()\n" + ex); result = 0; } finally { return result; } } function finishTable() { if (debugScript) window.alert("Beginning of function finishTable"); var tableElemName = "hikeTable"; var totalMilesPlanned = computeTableColumnTotal("hikeTable",2); var totalMilesHiked = computeTableColumnTotal("hikeTable",3); if (debugScript) { window.alert("totalMilesPlanned=" + totalMilesPlanned + "\n" + "totalMilesHiked=" + totalMilesHiked); } return; } </script>
The last part is the simplest—it is done by a simple application of Dynamic HTML techniques. The technique is explained in detail in the file topics/javascript/dynhtml/dynHtml.html, so here we'll just present the final code. Only the finishTable() function changes from the code in the previous step:
function finishTable() { if (debugScript) window.alert("Beginning of function finishTable"); var tableElemName = "hikeTable"; var totalMilesPlanned = computeTableColumnTotal("hikeTable",2); var totalMilesHiked = computeTableColumnTotal("hikeTable",3); try { var totalMilesPlannedElem = window.document.getElementById("totalMilesPlanned"); totalMilesPlannedElem.innerHTML = totalMilesPlanned; var totalMilesHikedElem = window.document.getElementById("totalMilesHiked"); totalMilesHikedElem.innerHTML = totalMilesHiked; } catch (ex) { window.alert("Exception in function finishTable()\n" + ex); } return; }