Using Javascript to Add Calculated Fields to Your Web Page

By Dr. Peter Wayne


Robert posed this question on the Web App Server Forum:
I am trying to reproduce calculations developed in desktop application for the web. Could someone please advise or direct.
Typical calculation

To predict an annualised cost for fuel consumption based on say several invoices over a period of say for example 160 days.

Calculation in desktop
Totalise the days in the days field
Totalise the cost from the same invoices in the cost field
Projected annualised cost=cost/days*(365)

Where in a web application can this type of calculation be included so that the user can view and use the result based on an appropriate filtered selection of invoices.
Robert had this working on his desktop application, but wanted to reproduce this functionality for the web app. In a desktop form, you can place a calculated field on the form and produce the projected fuel cost – but can you do the same in a web app? Unfortunately, although the component designer permits creation of summary calculated fields for multiple rows of a grid, it does not allow calculations on those summaries – at least not yet.

Why Javascript?

This sounds like a job for Javascript. Javascript is the language for examining and manipulating the objects on a web page. If the fuel consumption and the dates are on the page, Javascript can find them, and we simply have to learn enough Javascript to make Robert happy.

Alpha Five, as far as it takes us

For this example, I created a simple table with 3 fields:
Driver, C, 4 
Date, D
Gallons,N, 4, 0
and called the table fuel.dbf. I filled it in with some fake data for driver “1” and driver “2”:


Figure 1. Fuel.dbf with some data.
Then I created a web component to show the table. The component was created as Updateable with a Search part:

Figure 2. The Grid contains a 'Search' part.
I designed the grid to show the records in descending date order:

Figure 3. Choose an order for the grid component.
And I'm going to show all 3 of these fields on the grid:
Figure 4. The 3 fields will appear on the grid.
Now here's where I make a significant choice. If I want to use Javascript to examine all the records selected by the user, then all the records must be visible on the page at the same time. That is, I cannot allow Alpha Five to split the records over multiple “pages”. I can force Alpha Five to display all the records on a single web page in the “Properties” for the component:

Figure 5. Set “Rows of data” to 0 to display all the records in a single page.
Now on to the Search portion of the component. I decided Robert would most likely only need to search by Date and Driver, so those are the only fields I included in the Search:
Figure 6. Specify the fields for the Search portion of the grid.
And now, since I told Alpha Five to display all the records on one page, I absolutely MUST hide the grid when no search is active. Do you see why? If I don't, then when I start the application without an active search, Alpha Five will send the entire table over the internet to the user's browser – which might take quite a while after a few thousand records are stored in the table!

Figure 7. Click on “Hide Grid when no search active” to prevent Alpha Five from sending the entire table out when no search is in effect.
So much for the grid. Save the component (fuel.a5wcmp) and then save a page based on the component (fuel.a5w). If we look at the page in a browser with an active search, we see a functional grid:

Figure 8. Grid component.

And now, enter Javascript

Now here is where we need Javascript to run through the table, calculate the gallons used over time, and project future usage. In your browser, look at the source code generated by Alpha Five (click on View, Source). You'll see lots of lines that look like this:
<input id="V.R1.DRIVER" size="4" maxlength="4" class="AlphaSportsInput" name="V.R1.DRIVER" value="1" type="text">

<input id="V.R1.DATE" size="10" maxlength="10" class="AlphaSportsInput" name="V.R1.DATE" value="09/05/2005" type="text">
<a onclick="load_date_picker('V.R1.DATE',document.getElementById('V.R1.DATE').value, fuel_DATE_DateSet)">
<img src="css/CalendarIcons/Calendar2.png" border="0"></a>
<input id="V.R1.GALS" style="text-align: right;" size="4" maxlength="4"
class="AlphaSportsInput" name="V.R1.GALS" value="18" type="text">
I highlighted the important point, which is that every input field in every row has a unique ID. We can use the ID to reference each input field in Javascript, using the Javascript method, document.getElementById(). That is, if I write
obj=document.getElementById(“V.R1.DRIVER”)
then
obj 
refers to the “Driver” field in the first row, and
 obj.value
retrieves the contents of that field. This is not the place for a complete Javascript tutorial, and I'm far from the person best qualified to do it in any event! However I am going to show you how much can be done with just a smattering of Javascript. The only important features of Javascript that we are going to use, other than flow control structures (if, while, function calls, etc.) are the document.getElementById() method and object.innerHTML assignment, which lets us assign HTML coding to an object – in this example, to a table data cell. I'm also going to use a little CSS to position the results nicely, to get the results shown below:

Figure 9. Including calculated fields on the page.
Here's the hand-modified source code for fuel.a5w that generates the above page view. I highlighted the parts that I had to add:
<html>
<head>

<script type="text/javascript" language="javascript">

function fuelUsed(){
var drivers="";
i=1;
var gals=0;
var rows=0;
// the while loop goes through the entire table
while (document.getElementById("V.R"+i+".GALS")) {
obj=document.getElementById("V.R"+i+".GALS");
// check to see if a number is in the field, as opposed to a blank field
if (parseFloat(obj.value)){
rows+=1;
gals+=parseFloat(obj.value);
driver=document.getElementById("V.R"+i+".DRIVER").value;
if (drivers.indexOf(driver+",")==-1)
drivers+=driver+",";
}
i+=1;
} //end while
if (rows>0) {
obj1=document.getElementById("V.R1.DATE");
if (obj1){
startDate=new Date(obj1.value);
endDate=new Date(document.getElementById("V.R"+rows+".DATE").value);
// subtracting days yields milliseconds, convert to days
diffDays=Math.abs((endDate-startDate)/(1000*60*60*24));
// now fill in the results
galObj=document.getElementById("gallons");
galObj.innerHTML=gals;
daysObj=document.getElementById("days");
daysObj.innerHTML=diffDays;
projectedObj=document.getElementById("projected");
projectedObj.innerHTML=Math.round(100*gals*365/diffDays)/100+" gallons";
drivArray=drivers.split(',');
document.getElementById("breakdownLabel").innerHTML="Breakdown by driver:"
for (i=0;i<drivArray.length-1;i++) {
driverObject=new Object();
driverObject.aDriver=drivArray[i];
driverObject.gals=0;
for (j=1;j<=rows;j++) {
driverObject.gals+=
document.getElementById("V.R"+j+".DRIVER").value ==
driverObject.aDriver?parseFloat(document.getElementById("V.R"+j+".GALS").value):0;
}
document.getElementById("breakdown").innerHTML+=
"<br />Driver "+driverObject.aDriver+": "+driverObject.gals+" gals.";
}
reslts=document.getElementById("right");
reslts.style.display=' block';
}
} else {
reslts=document.getElementById("right");
reslts.style.display=' none';
}
}
function init(){
i=1;
while (document.getElementById("V.R"+i+".GALS")) {
obj1=document.getElementById("V.R"+i+".DATE");
obj1.onchange=fuelUsed;
obj2=document.getElementById("V.R"+i+".GALS");
obj2.onchange=fuelUsed;
i+=1;
}
fuelUsed();
}
window.onload=init;
</script>

<style type="text/css">
#left {
float: left;
}
#results {
position:relative;
top:2em;
left:1em;
border:1ex solid blue;
}
</style>

<%a5
Delete Tmpl
DIM Tmpl as P
tmpl = a5w_load_component("fuel")
'Following code allows you to override settings in the saved component, and specify the component alias (componentName
property).
'Tip: Keep the componentName property short because this
name is used in page URLs, and it will help keep the URLs short.
'Each component on a page must have a unique alias
(componentName property).
with tmpl
componentName = "fuel"
end with '=======================================compute
the HTML for the Component=======================================
delete x_out
dim x_out as p
tmpl.request = request
tmpl.session = session
tmpl.response = response
tmpl.serversetting = serversetting
tmpl.PageVariables = local_variables()
x_out = a5w_run_Component(tmpl)
'=============================================================================================================
if x_out.RedirectURL <> "" then
response.redirect(x_out.redirectURL)
end
end if
?x_out.Output.Head.JavaScript
?x_out.Output.Head.CSS_Link
%>

<!--Alpha Five Temporary Code Start - Will be automatically removed when page is published -->
<!--CSS for tmpl -->
<link rel="stylesheet" type="text/css"
href="file:///C:\Program Files\A5V6/css/AlphaSportsSmall/style.css">
<!--Alpha Five Temporary Code End -->
<title>fuel</title>
</head>
<%a5 ?x_out.Output.Body.Body_Tag %>
<!--Alpha Five Temporary Code Start - Will be automatically removed when page is published -->
<!--Body Tag for tmpl -->

<body class="AlphaSportsSmallPageBODY"><!--Alpha Five Temporary Code End --><!-- Any text that you want to output above the component
goes here-->
<br>
<div id="left">
<table>
<tr>
<td><%A5 ?x_out.Output.Body.Grid_Echo %></td>
</tr>
<tr>
<td><%A5 ?x_out.Output.Body.UpdateErrors %></td>
</tr>
<tr>
<td><%A5 ?x_out.Output.Body.Search_HTML %></td>
</tr>
<tr>
<td><%A5 ?x_out.Output.Body.Grid_HTML %></td>
</tr>
<tr>
<td><%A5 ?x_out.Output.Body.DetailView_HTML %></td>
</tr>

</table>
</div>
<div id="right">
<table id="results">
<tr>
<td>Gallons used: </td><td id="gallons"></td>
</tr>
<tr><td>Over days: </td><td id="days"></td>
</tr>
<tr><td>Projected one year usage:</td><td id="projected"></td>
</tr>
<tr><td id="breakdownLabel"></td><td id="breakdown"></td></tr>
</table>
</div>

</body>
</html>

Explanation of the script

    The modifications to the script are in three forms:
(1) a new web table, "results", to hold the results of the calculation
(2) stylesheet (CSS) entries to format the results,  and
(3) Javascript code to perform the calculations and (optionally) show the results.

(1) The New Web Table

Don't confuse a web table with an Alpha Five table. A web table is used simply to help format and display the results of the calculations -- it only exists on the web page. The web table is defined as
<table id="results">
<tr>
<td>Gallons used: </td><td id="gallons"></td>
</tr>
<tr><td>Over days: </td><td id="days"></td>
</tr>
<tr><td>Projected one year usage:</td><td id="projected"></td>
</tr>
<tr><td id="breakdownLabel"></td><td id="breakdown"></td></tr>
</table>
The table is given an ID of "results", so that Javascript and CSS can easily refer to it later. It has three rows and each row has two cells: the first cell in each row contains a label, e.g., "Gallons used:", and the second cell will hold the calculated results.  The cells that will hold calculated results also have IDs so that I can easily use Javascript to put HTML into them.

(2) The CSS entries

The entire "results" table is contained in a division or HTML div given an ID of "right". All the output generated by Alpha Five is placed in a div with an ID of "left".  I then added this stylesheet to the "head" section of the A5W page:
<style type="text/css">
#left {
float: left;
}
#results {
position:relative;
top:2em;
left:1em;
border:1ex solid blue;
}
</style>
It's hard to get simpler than this. What is says is that the "left" div will "float" on the left side of the browser window, and the next div will appear, well, to its right!  The table named "results" will be positioned 2 ems (the size of an "M" in the current text font) from the top of its "container", 1 em to the left of its container, and with a solid blue border of width 1 ex (the size of an "x" in the current font).  What is the "container" for the table "results"?  It is the div we called "right" which appears to the right of the div called "left". Please note that the names of "left", "right", and "results" that I used are completely arbitrary -- the output formatting depends on the CSS styling, not on the identifiers used!

(3) The Javascript

Next we come to the actual Javascript code. In outline form, the Javascript code consists, again, of three parts:
<script type="text/javascript" language="javascript">
function fuelUsed(){
..with some code
}

function init(){
..with some code
}
window.onload=init;
</script>


The curly braces { and } mark the beginning and end of the multiline functions. The line
window.onload=init; 
instructs a Javascript-enabled browser to execute the "init" function after the page content has finished loading, but before it is displayed. So let's look at what is in the "init" function? Expanded, it reads
function init(){
i=1;
while (document.getElementById("V.R"+i+".GALS")) {
obj1=document.getElementById("V.R"+i+".DATE");
obj1.onchange=fuelUsed;
obj2=document.getElementById("V.R"+i+".GALS");
obj2.onchange=fuelUsed;
i+=1;
}
fuelUsed();
}

The function initializes a local variable, "i", and sets it to "1". The next line is a little odd if you haven't used Javascript before, but believe me, it's one of the most powerful features of Javascript.  In the first iteration through the while loop the line will read
while (document.getElementById("V.R1.GALS")){
..do the loop
}
This while is testing for the existence of an object with an ID of  "V.R1.GALS".  I don't have to know in advance how many rows of the fuel table Alpha Five is passing to the user's browser -- I can use this while loop to run through the table. For each row in the table, I then add an onchange event to the cells that contain dates or fuel usage; if the user changes those values, it will invoke the fuelUsed function and recalculate the projections even before the new values are saved.
The final line of init calls the fuelUsed function. fuelUsed() is the longest and most complex function in the script. I'll reproduce it below with commentary as we go through. Incidentally, // signals the beginning of a single-line comment in Javascript; multi-line comments can be demarcated with /* and */:
function fuelUsed(){
//initialize a few variables
var drivers="";
i=1;
var gals=0;
var rows=0;
// the while loop goes through the entire table
while (document.getElementById("V.R"+i+".GALS")) {
obj=document.getElementById("V.R"+i+".GALS");
/* check to see if a number is in the field, as opposed to a blank field
parseFloat() is a built-in Javascript function that tries to make a number out of
what it's been given. Therefore if parseFloat(obj.value) succeeds, it tests as true,
otherwise it returns something called NaN (Not a Number), or false.
I also use Javascript shorthand for addition, e.g.
rows=rows+1;
can be written in shorthand as
rows+=1;
*/
if (parseFloat(obj.value)){
rows+=1; // rows contains the number of non-empty rows
gals+=parseFloat(obj.value);
driver=document.getElementById("V.R"+i+".DRIVER").value;
/* I make up a string consisting of all the drivers separated by commas. If the
driver is not found in the string (tested with the indexOf built-in Javascript function),
then I add the driver to the string.
*/
if (drivers.indexOf(driver+",")==-1)
drivers+=driver+",";
}
i+=1;
} //end while

if (rows>0) {
obj1=document.getElementById("V.R1.DATE");
if (obj1){
startDate=new Date(obj1.value);
endDate=new Date(document.getElementById("V.R"+rows+".DATE").value);
/* Javascript has a built-in Date object, so we can get a date by feeding it
a string in a number of formats. Addition and subtraction of dates, though,
is done in milliseconds. When we subtract Javascript dates from one another, therefore,
we must divide by 1000*60*60*24 to get the actual number of days!
*/
diffDays=Math.abs((endDate-startDate)/(1000*60*60*24));
// now fill in the results
galObj=document.getElementById("gallons");
galObj.innerHTML=gals;
daysObj=document.getElementById("days");
daysObj.innerHTML=diffDays;
projectedObj=document.getElementById("projected");
/* Here we go. "innerHTML" is actually not standards-compliant scripting, but since all
browsers support it, I am using it here. It is a heck of a lot simpler to code with
"innerHTML" than it is to code with standards-compliant Javascript, which requires us to
add "nodes" to the "parent nodes" of objects. Ugh!
And "Math.round()" is a built-in Javascript function using the "Math object". Not
surprisingly, it rounds numbers to the nearest integer.
*/
projectedObj.innerHTML=Math.round(100*gals*365/diffDays)/100+" gallons";
/* I take the "drivers" list, which can look something like "2,1,", and put it in an
array called "drivArray" using the Javascript string-splitting function.
*/
drivArray=drivers.split(',');
// Here we use the 'innerHTML' again.
document.getElementById("breakdownLabel").innerHTML="Breakdown by driver:"
for (i=0;i<drivArray.length-1;i++) {
driverObject=new Object(); //create a new Object, like a pointer in Xbasic
driverObject.aDriver=drivArray[i];
driverObject.gals=0;
// I'll explain the next part separately below!

for (j=1;j<=rows;j++) {
driverObject.gals+=
document.getElementById("V.R"+j+".DRIVER").value ==
driverObject.aDriver?parseFloat(document.getElementById("V.R"+j+".GALS").value):0;
}
document.getElementById("breakdown").innerHTML+=
"<br />Driver "+driverObject.aDriver+": "+driverObject.gals+" gals.";
}
// if there are non-empty rows, then we display the "right" div
reslts=document.getElementById("right");
reslts.style.display=' block';
}
} else {
/* if there are no non-empty rows, e.g., only the Search part is active, then
don't display the "right" div
*/
reslts=document.getElementById("right");
reslts.style.display=' none';
}
}

A little for loop in the above script requires some extra explanation.  The loop begins
for (j=1;j<=rows;j++) { 
..some processing
}
This syntax is equivalent to the Xbasic syntax of
for j=1 to rows
..some processing
next j

The processing in the for loop also cries out for a little exegesis:
driverObject.gals+=
document.getElementById("V.R"+j+".DRIVER").value ==
driverObject.aDriver?parseFloat(document.getElementById("V.R"+j+".GALS").value):0;
}

I've used the Javascript ternary operator, (testval)?(truthaction):(falseaction).  We have a similar operator in Alpha Five, the in-line if statement, if(testval,truthaction,falseaction).  You should also be aware that the test for equality in Javascript is ==, different from the assignment operator of =.  So what this for loop does is, for each driver, go through the rows of the table as present in the browser, and if the row pertains to that driver, add the gallons to that driver's subtotal, otherwise add "0" to the subtotal.

This was a good tour through Javascript's capabilities, but Robert is not easily satisfied. He adds
And if going a step further comparing projected cost against past costs and displaying results on chart on the web.

I did not measure past costs, but I did make a slight modification that enabled me to generate a simple bar graph giving a visual representation of how much fuel was used by each driver:





But that's an exercise left to the reader!

9/10/2005

Don't forget, we need your feedback to make this site better!

Return to home