
StrategyThis discussion explains the implementation of a calculator widget using asynchronous communication between a web page and the web server without reloading the web page -- a methodology that has come to be known as AJAX.
HTML layout
Javascript event handlers
The "CLEAR" button press
The server call back
Summary
The embedded frame below displays the calculator application
described here.
Please try it out by pushing the buttons and typing values into the input element.
calc.whiff will maintain four state
variables contained in HTML form elements:
<input id="value" name="value" type="hidden" value="0">
value variable will hold the "old value" waiting
to be combined (added or whatever) with the displayed value.
<input id="operation" name="operation" type="hidden" value="">
operation variable will store the current operation
(like "+")
which is waiting for the arguments to be complete
<input id="display" name="display" value="0">
display variable will store value most recently entered
by the user in the calculator.
<input id="stack" name="stack" type="hidden" value="">
stack variable will store a sequence of previous computations
stored for future reference.
In addition to the state variables the calc.whiff page
will provide a message for displaying the current
state of the calculator.
<div id="message"> ready </div>
For the purposes of illustration we will pretend that we need to call back to the server in order to implement the calculator button operations such as "CLEAR" and "+".
To call back to the server the calculator page will send a request to the server that emulates a form submission specifying the CGI values for the four state variables.
In response to the request the server callback implementation
calcCallback.py will send a fragment of javascript
implementing appropriate changes to the state variable
and the message area. For example if the value
is 7 and the display shows 3 when the current
operation is +, then a response to an ENTER press
generates the following javascript response
(function () {
var elt = document.getElementById("message");
elt.innerHTML = "7.0 + 3.0 = 10.0";
} ) ();
(function () {
var elt = document.getElementById("display");
elt.value = "10.0";
} ) ();
(function () {
var elt = document.getElementById("value");
elt.value = "10.0";
} ) ();
(function () {
var elt = document.getElementById("operation");
elt.value = "";
} ) ();
When the calc web page receives the javascript response it executes the response
to effect the changes to the page.
calc.whiff configuration template
which specifies the layout of the calculator using a table and also declares the state
variables for the calculator and the buttons, with attached mouse events.
{{env whiff.content_type: "text/html"/}}
<html>
<head>
<title>Ajax calculator demo</title>
</head>
<body style="font-family: Verdana, Arial, Helvetica, sans-serif;">
<script src="whiff_middleware/whiff.js"></script>
<form action="calc">
<input id="value" name="value" type="hidden" value="0">
<input id="stack" name="stack" type="hidden" value="">
<input id="operation" name="operation" type="hidden" value="">
<div id="message"> ready </div>
<table bgcolor="#555577">
<tr>
<th colspan=4"> calculator </th>
</tr>
<tr>
<th colspan="3"> <input id="display" name="display" value="0"> </th>
<th> <button onclick="callBack('clear');return false;">
Clear</button> </th>
</tr>
<tr>
<th> <button onclick="numeric(7);return false;">
7 </button> </th>
<th> <button onclick="numeric(8);return false;">
8 </button> </th>
<th> <button onclick="numeric(9);return false;">
9 </button> </th>
<th> <button onclick="callBack('/');return false;">
/ </button> </th>
</tr>
<tr>
<th> <button onclick="numeric(4);return false;">
4 </button> </th>
<th> <button onclick="numeric(5);return false;">
5 </button> </th>
<th> <button onclick="numeric(6);return false;">
6 </button> </th>
<th> <button onclick="callBack('x');return false;">
X </button> </th>
</tr>
<tr>
<th> <button onclick="numeric(1);return false;">
1 </button> </th>
<th> <button onclick="numeric(2);return false;">
2 </button> </th>
<th> <button onclick="numeric(3);return false;">
3 </button> </th>
<th> <button onclick="callBack('+');return false;">
+ </button> </th>
</tr>
<tr>
<th> <button onclick="numeric(0);return false;">
0 </button> </th>
<th> <button onclick="numeric('.');return false;">
. </button> </th>
<th> <button onclick="callBack('neg');return false;">
(-)</button> </th>
<th> <button onclick="callBack('-');return false;">
- </button> </th>
</tr>
<tr>
<th> <button onclick="callBack('push');return false;">
psh</button> </th>
<th> <button onclick="callBack('pop');return false;">
pop</button> </th>
<th colspan="2"> <button onclick="callBack('ENTER');return false;">
ENTER</button> </th>
</tr>
</table>
... TO BE CONTINUED BELOW ...
</form>
numeric
<button onclick="numeric(0);return false;"> 0 </button>
callBack
<button onclick="callBack('ENTER');return false;"> ENTER</button>
calc.whiff configuration which defines
the javascript functions to handle the button press events for the calculator.
... CONTINUED...
<script>
function numeric(num) {
var display = document.getElementById("display");
var value = display.value;
if (value=="0") {
display.value = ""+num;
} else {
display.value = display.value + (""+num);
}
}
function callBack(mode) {
{{include "whiff_middleware/runAsyncJavascript" Url: "calcCallBack"}}
mode: mode
{{/include}}
}
</script>
</body></html>
numeric event handler blindly appends the corresponding
character to the value of the display input element.
The callBack event handler implements the AJAX callback
to the server which implements the "operation" button presses, using the
whiff_middleware/runAsyncJavascript standard middleware.
The whiff_middleware/runAsyncJavascript middleware generates
a javascript code fragment which sends a page configuration back to the
web server. In this case it sends the page
{{include "calcCallBack"/}}
EvalPageFunction requestor. The
EvalPageFunction requestor recieves and evaluates the response
text as javascript.
The EvalPageFunction implementation automatically passes
form element values from the page to the web server. When the web
server recieves the request the form element values are installed as
CGI parameters for the page evaluation. For this example the
state variables display, value, stack, and operation
are sent as CGI values for the expansion of the calcCallBack.whiff
The EvalPageFunction implementation allows callbacks to add
additional arbitrary values which are also installed as CGI values
for the page evaluation. In this case the parameter
mode: mode
calcCallBack.whiff
expansion with the name "mode" and the value determined
by the value of the javascript variable mode.
display = "65";
value = "-13";
stack = "44 898";
operation = "+";
message = "-13 +"
"callBack('clear');return false;"
callBack function extracts form element values
and appends the "mode" variable value, encoding the following values
as CGI parameters to be sent to the server
display = "65";
value = "-13";
stack = "44 898";
operation = "+";
mode = "clear";
callBack function constructs an HTTP request
for the web server to expand the configuration template
{{include "calcCallBack"/}}
{ "display": "65", "value": "-13", "stack": "44 898",
"operation": "+", "mode": "clear" }
{{include "calcCallBack"/}}
calcCallback page
generates the javascript code
(function () {
var elt = document.getElementById("message");
elt.innerHTML = "cleared";
} ) ();
(function () {
var elt = document.getElementById("display");
elt.value = "0";
} ) ();
EvalPageFunction requestor in the web browser.
calc page,
changing the values displayed by the message area and the display input element.
display = "0";
value = "-13";
stack = "44 898";
operation = "+";
message = "cleared"
calcCallBack
is shown below. The calcCallBack uses the values of the
emulated form input parameters display, value, stack, operation and mode
to generate an appropriate javascript fragment response for updating the calc
page.
from whiff.middleware import misc
from whiff import resolver
from whiff import whiffenv
BINARY_OPERATIONS = ["/", "+", "-", "x"]
UNARY_OPERATIONS = ["neg", "push", "pop"]
class calcCallBack(misc.utility):
def __call__(self, env, start_response):
env = resolver.process_cgi(env, parse_cgi=True)
mode = whiffenv.cgiGet(env, "mode", "")
display = whiffenv.cgiGet(env, "display", "0")
value = whiffenv.cgiGet(env, "value", "0")
stack = whiffenv.cgiGet(env, "stack", "")
operation = whiffenv.cgiGet(env, "operation", "")
proxy = Proxy(display, value, stack, operation)
# validate the display value
try:
displayValue = float(display)
except ValueError:
displayValid = False
else:
displayValid = True
if mode=="clear":
proxy.display = "0"
proxy.operation =""
proxy.message = "cleared"
elif not displayValid:
proxy.message = "invalid numeric data"
proxy.value = "0"
elif mode=="ENTER":
self.doOperation(proxy, value, operation, display)
proxy.operation = ""
elif mode in BINARY_OPERATIONS:
self.doOperation(proxy, value, operation, display)
proxy.operation = mode
proxy.message = "%s %s" % (proxy.display, mode)
proxy.display = 0
elif mode in UNARY_OPERATIONS:
self.doOperation(proxy, value, operation, display)
self.doUnary(proxy, mode)
else:
proxy.message = "unknown mode ("+mode+")"
response = proxy.javascript()
#response = "alert('hello from calcCallBack')"
start_response("200 OK", [('Content-Type', 'application/javascript')])
return [response]
def doUnary(self, proxy, mode):
display = float(proxy.display)
if mode=="neg":
proxy.display = proxy.value = -display
elif mode=="push":
proxy.stack = proxy.stack+" "+str(proxy.display)
proxy.message = "pushed "+proxy.display
elif mode=="pop":
sstack = proxy.stack.split()
if not sstack:
proxy.message = "stack underflow"
else:
popped = sstack[0]
proxy.stack = " ".join(sstack[1:])
proxy.display = popped
proxy.message = "popped "+str(popped)
else:
proxy.message = "unknown unary mode ("+mode+")"
def doOperation(self, proxy, value, operation, display):
v = float(value)
d = float(display)
if operation=="/":
if d==0:
proxy.message = "divide by zero not defined"
proxy.display = 0
else:
proxy.display = proxy.value = r = v/d
proxy.message = "%s / %s = %s" % (v,d,r)
elif operation=="x":
proxy.display = proxy.value = r = v*d
proxy.message = "%s x %s = %s" % (v,d,r)
elif operation=="+":
proxy.display = proxy.value = r = v+d
proxy.message = "%s + %s = %s" % (v,d,r)
elif operation=="-":
proxy.display = proxy.value = r = v-d
proxy.message = "%s - %s = %s" % (v,d,r)
elif operation=="":
# no operation, copy display to value
proxy.value = proxy.display
else:
proxy.message = "unknown operation ("+operation+")"
proxy.display = 0
return proxy
def setMessage(message):
"create javascript to display a message"
return """
(function () {
var elt = document.getElementById("message");
elt.innerHTML = "%s";
} ) ();
""" % (message,)
def setValue(identity, value):
"create a javascript fragment to set an input element value"
return """
// create and call anonymous function
(function () {
var elt = document.getElementById("%s");
elt.value = "%s";
} ) ();
""" % (identity, value)
class Proxy:
def __init__(self, display, value, stack, operation):
self.display = self.display0 = display
self.value = self.value0 = value
self.stack = self.stack0 = stack
self.operation = self.operation0 = operation
self.message = None
def javascript(self):
result = ""
if self.message:
result += setMessage(self.message)
if self.display0!=self.display:
result += setValue("display", self.display)
if self.value0!=self.value:
result += setValue("value", self.value)
if self.stack0!=self.stack:
result += setValue("stack", self.stack)
if self.operation0!=self.operation:
result += setValue("operation", self.operation)
return result
__middleware__ = calcCallBack
calcCallBack is implemented as a standard
WHIFF application using methods already discussed in the previous tutorials.
The only trick worthy of note in the implementation is the use of a "client proxy" object which keeps track of changes in the state variables and generates appropriate javascript fragments reflecting the changes.
whiff_middleware/EvalPageFunction provided the
primary functionality for configuring a request from the client browser to be
evaluated at the server.