To make our life easier while writing the Javascript functions, let's make use of some functions from the Mochikit library. In order for TurboGears to include Mochikit in every served HTML, modify the following configuration in ajaxfin/config/app.cfg:
# Set to True if you'd like all of your pages to include MochiKit tg.mochikit_all = True
Since Mochikit is a static file, the browser will likely keep it cached, so adding it in every page is not likely to waste bandwidth.
Now let's develop our own Javascript code to put our calculator in working order. The whole code described below must be copied to ajaxfin/static/javascript/fincalc.js file. If you prefer, you can get the source file from the finished project.
var controls_str = [ "n", "i", "PV", "PMT", "FV" ]; var ajax_control = null;
Two global variables that will be useful at some point below.
function begin_toogle()
{
if (parseInt(document.calc.begin.value) == 0) {
document.calc.begin.value = 1;
document.calc.Button_begin.className = "mybutton_1";
} else {
document.calc.begin.value = 0;
document.calc.Button_begin.className = "mybutton_0";
}
}
function stoeex_toogle()
{
if (parseInt(document.calc.stoeex.value) == 0) {
document.calc.stoeex.value = 1;
document.calc.Button_stoeex.className = "mybutton_1";
} else {
document.calc.stoeex.value = 0;
document.calc.Button_stoeex.className = "mybutton_0";
}
}
Slow start. These functions toggle value and color of the "FPC" and "Begin" buttons. Don't worry, I will explain their meanings till the end of tutorial.
function fincalc_fin_chk(op)
{
var ret = true;
var controls = [ document.calc.n, document.calc.i, document.calc.PV, document.calc.PMT, document.calc.FV ];
var control = controls[0];
for(var x = 0; x < controls.length; ++x) {
if (op == controls_str[x]) {
control = controls[x];
}
v = parseFloat(controls[x].value);
if (isNaN(v) & op != controls_str[x]) {
controls[x].value = "?";
ret = false;
}
}
return ret;
}
When the user presses a financial button, this function checks whether the arguments are valid.
function fincalc_math_chk(op)
{
var ret = true;
var p1 = parseFloat(document.calc.PV.value);
var p2 = parseFloat(document.calc.PMT.value);
if (op != "inv") {
if (isNaN(p1)) {
document.calc.PV.value = "?"
ret = false;
}
}
if (isNaN(p2)) {
document.calc.PMT.value = "?"
ret = false;
}
return ret;
}
This function checks the arguments when an arithmetic operation is called (which have at most 2 arguments -- the yellow form fields).
function fincalc(op)
{
if (ajax_control != null) {
return false;
}
var decimals = parseInt(document.calc.decimals.value);
if (isNaN(decimals) || decimals > 9) {
decimals = 2;
document.calc.decimals.value = "2";
}
var stoeex = (parseInt(document.calc.stoeex.value) != 0 ? 1 : 0);
var begin = (parseInt(document.calc.begin.value) != 0 ? 1 : 0);
if (op == "i" || op == "n" || op == "PV" || op == "PMT" || op == "FV") {
// financial
if (fincalc_fin_chk(op))
fincalc_fin(op, decimals, stoeex, begin);
} else {
if (fincalc_math_chk())
fincalc_math(op, decimals);
}
return true;
}
This one is invoked when a button is pressed. It dispatches the argument check as well as the calculation. Note that it returns early when ajax_control is not null (which means that there is a pending AJAX operation in progress).
function fincalc_math(op, decimals)
{
var p1 = parseFloat(document.calc.PV.value);
var p2 = parseFloat(document.calc.PMT.value);
var control = document.calc.FV;
jkeys = ["tg_format", "p1", "p2", "op" ];
jvalues = ["json", p1, p2, op];
addr = "/fincalc_math?" + queryString(jkeys, jvalues);
if (op == "inv") {
control = document.calc.PMT;
}
ajax_control = control;
ajax_control.value = "Wait...";
ajax_control.decimals = decimals;
ajax = loadJSONDoc(addr);
ajax.addCallbacks(fincalc_math_callback, fincalc_math_callback_err);
// now we pray :)
}
Finally the first function effectively involved with AJAX; it invokes the remote call to have the arithmetic operation done.
It builds two lists: the first with parameters' names (jkeys), the second with the respective parameters' values (jvalues), and builds the HTTP GET call (addr) based on that lists. Finally, it invokes loadJSONDoc() and adds the callback functions that are to be called when the remote call is done.
Once invoked, the function returns immediately (asynchronous call), as if the story ended here. The browser goes back to the user. Only when the call returns, it will resume the Javascript execution (the callback functions).
Note that we fill ajax_control global variable with a form field. This serves two purposes: point to the callbacks where to show the results, and to avoid having more than one AJAX call in flight at the same time (fincalc() returns shortly when ajax_control is not null).
By using loadJSONDoc() instead of XMLHttpRequest(), we must ensure that the server side will return JSON-encoded data, not XML.
"But what the hell is JSON?" It is an encoding lighter than XML, which can be directly interpreted by the browser Javascript engine -- it is actually laid as a piece of Javascript source code (a really clever idea).
JSON is considered unsafe compared to XML, since a carefully crafted JSON response could explore some browser weakness. It is important not to use it when the server is not trustable. In the case of untrusted/unknown servers, XML requests are best.
function fincalc_math_callback(rres)
{
var val = parseFloat(rres.result);
var err = parseFloat(rres.error);
if (err != 0) {
ajax_control.value = "Error";
} else {
ajax_control.value = roundToFixed(val, ajax_control.decimals);
}
ajax_control = null;
}
function fincalc_math_callback_err(err)
{
ajax_control.value = "Remote err";
ajax_control = null;
}
These are the callback functions. One of them is called when the remote call initiated by loadJSONDoc is finished or fails. Note how we extract the return parameters (rres.result and rres.error).
Once the result is attributed to the form field, ajax_control is reset to null again, so the user is free to post a new request now.
function fincalc_fin(op, decimals, stoeex, begin)
{
var controls = [ document.calc.n, document.calc.i, document.calc.PV, document.calc.PMT, document.calc.FV ];
var values = [0, 0, 0, 0, 0]
var nop = -1;
for(var x = 0; x < controls.length; ++x) {
values[x] = parseFloat(controls[x].value);
if (op == controls_str[x]) {
nop = x;
}
}
if (nop == -1) {
return;
}
jkeys = ["tg_format", "nop", "n", "i", "pv", "pmt", "fv", "stoeex", "begin" ];
jvalues = ["json", nop, values[0], values[1], values[2], values[3], values[4], stoeex, begin];
addr = "/fincalc_fin?" + queryString(jkeys, jvalues);
ajax_control = controls[nop];
ajax_control.value = "Wait...";
ajax_control.decimals = decimals;
ajax = loadJSONDoc(addr);
ajax.addCallbacks(fincalc_fin_callback, fincalc_fin_callback_err);
// now we pray :)
}
This is the JSON dispatcher for the financial operations. Except by the bigger parameter count, it works exactly the same as the arithmetic operation dispatcher.
function fincalc_fin_callback(rres)
{
var val = parseFloat(rres.result);
var err = rres.error;
if (err.length > 0)
ajax_control.value = err;
else
ajax_control.value = roundToFixed(val, ajax_control.decimals);
ajax_control = null;
}
function fincalc_fin_callback_err(err)
{
ajax_control.value = "Remote err";
ajax_control = null;
}
And finally, the callback functions for the financial operations, that get the result and show it on screen.
Now we can reload the page and try to make some operation in the calculator:
It returned an error... Of course so; the server side still does not implement the fincalc_fin and fincalc_math remote calls. That's what we will do next.
Next: the calculation function at Controller side >>>