Dalke Scientific Software: More science. Less time. Products

Ajax

Ajax refers to a set of techniques for building highly interactive web pages. An essential one is using Javascript to have the browser talk to the server without having to load a new page.

The request is built using a low-level function called XMLHttpRequest. Different browsers implement it differently. Mochikit makes its own interface which is a wrapper for the underlying toolkit, to isolate your code from browser specific code. It's also more elegant and easier to use.

The first "A" of "Ajax" stands for "Asynchronous". The network connection or the server may be slow and you don't want the browser to wait around until the server is finished. Otherwise the browser will be locked up and unresponsive. In an asynchronous request the request is made and control returns to Javascript. In the background the browser will save the response from the server. When done it calls a designated Javascript function to process the downloade data.

The data can be in any format. Most people using AJAX pass the data in JSON format but it really can be anything. I'll show using JSON.

I'll make a taxonomy lookup service which does not use a form. When the enter key is pressed it in the text widget it will send a request to the server and display the results.

demo 1

For the first example I'll get a version working which does not use the server.

<script type="text/javascript">
var default_response = {"id": "9606", "species": "homo sapiens"};

var make_request1 = function (e) {
  /* only listen for the enter key */
  if (e.key().string != "KEY_ENTER") {
    return;
  }

  /* Fake a response from the server */
  var response = default_response;

  var result = $("result1");
  replaceChildNodes(result, response.species);
}

</script>

<div style="background: #EEE">
<input id="tax1" size="7">
<span id="result1"></span>
</div>

<script type="text/javascript">
/* Listen for the enter key; use 'onkeyup' for non-printable keys */
connect("tax1", "onkeyup", make_request1);
</script>

demo 2

I'll modify the code to make it asynchronous. The 'callLater' function is a MochiKit function which calls the given function the given time in the future.

<script type="text/javascript">

var default_response = {"id": "9606", "species": "homo sapiens"};

var make_request2 = function (e) {
  /* only listen for the enter key */
  if (e.key().string != "KEY_ENTER") {
    return;
  }
 
  /* Call the 'show_result2' function 2 seconds from now */
  callLater(2, show_result2, default_response);
}

var show_result2 = function (response) {
  var result = $("result2");
  replaceChildNodes(result, response.species);
}

</script>

<div style="background: #EEE">
<input id="tax2" size="7">
<span id="result2"></span>
</div>

<script type="text/javascript">
/* Listen for the enter key; use 'onkeyup' for non-printable keys */
connect("tax2", "onkeyup", make_request2);
</script>

demo 3

This shows how to use MochiKit to load a JSON file named "human.json". Here is the content of that file

{ "id": "9606", "species": "homo sapiens" }
JSON stands for "JavaScript Object Notation" and as you can see this is identical to the existing "default_response" variable.

The MochiKit code to load a JSON doc module is in two steps. First specify the URL to load and second then specify the function to call when the document is loaded.

<script type="text/javascript">

var make_request3 = function (e) {
  /* only listen for the enter key */
  if (e.key().string != "KEY_ENTER") {
    return;
  }

  /* get the JSON from a file */
  d = loadJSONDoc("human.json"); 
  d.addCallback(show_result3);
}

var show_result3 = function (response) {
  var result = $("result3");
  replaceChildNodes(result, response.species);
}

</script>

<div style="background: #EEE">
<input id="tax3" size="7">
<span id="result3"></span>
</div>

<script type="text/javascript">
/* Listen for the enter key; use 'onkeyup' for non-printable keys */
connect("tax3", "onkeyup", make_request3);
</script>

demo 4

Next I want to talk to a live server. This was a bit tricky as browswer security requires that the HTML page and the JSON requests are both from the same server. I ended up making a special controller to return the HTML needed for my ajax demo page.

    @expose()
    def human_json(self):
        return open("/Users/dalke/nbn/human.json").read()

    @expose()
    def ajax_html(self):
        return open("/Users/dalke/nbn/ajax.html").read()

I modified my existing taxonomy server to return the requested information. TurboGears understands JSON. By specifying 'format="jason"', it will convert the returned dictionary into JSON format and not pass it into a template.

    @expose(format="json")
    def get_taxon_info(self, taxid):
        taxon = Taxonomy.get(int(taxid))
        return dict(id=taxon.id, species=taxon.scientific_name)
The code is pretty basic and doesn't catch errors. I'll fix that in a moment.

Here's the new code to make the request. I get the originating DOM element by using event.src() and I pass the "taxid" query parameter as a dictionary in loadJSONDoc.


<script type="text/javascript">

var make_request4 = function (e) {
  /* only listen for the enter key */
  if (e.key().string != "KEY_ENTER") {
    return;
  }

  /* get the JSON from a server */
  d = loadJSONDoc("http://localhost:8080/get_taxon_info",
                  {"taxid": e.src().value});

  d.addCallback(show_result4);
}

var show_result4 = function (response) {
  var result = $("result4");
  replaceChildNodes(result, response.species);
}

</script>

<div style="background: #EEE">
<input id="tax4" size="7">
<span id="result4"></span>
</div>

<script type="text/javascript">
/* Listen for the enter key; use 'onkeyup' for non-printable keys */
connect("tax4", "onkeyup", make_request4);
</script>

demo 5

What happens if the network dies? What happens if there's an error in the server? As it is now there's no feedback in the client. MochiKit knows there is an error, you just have to tell it what to do when the error occurs by using the "addErrback" to the Defered object created by loadJSONDoc:

<script type="text/javascript">

var make_request5 = function (e) {
  /* only listen for the enter key */
  if (e.key().string != "KEY_ENTER") {
    return;
  }

  /* get the JSON from a server */
  d = loadJSONDoc("http://localhost:8080/get_taxon_info",
                  {"taxid": e.src().value});

  d.addCallback(show_result5);
  d.addErrback(show_err5);
}

var show_result5 = function (response) {
  var result = $("result5");
  replaceChildNodes(result, response.species);
}

var show_err5 = function (response) {
  var result = $("result5");
  replaceChildNodes(result, "<server failure>");
}

</script>

<div style="background: #EEE">
<input id="tax5" size="7">
<span id="result5"></span>
</div>

<script type="text/javascript">
/* Listen for the enter key; use 'onkeyup' for non-printable keys */
connect("tax5", "onkeyup", make_request5);
</script>

demo 6

The server code doesn't catch any errors. What happens if the reqested taxid is not an integer or not found? It will raise an error and return an "Error 400" code to the client.

I can catch some of the errors in the server. I'll modify the server to return status information and a possible error message when there is a problem. The new "status" element will either by "success" or "error" and if there is an error the error message will be in "errmsg". I've renamed it 'get_taxon_info6' so my original 'get_taxon_info' will still generate errors for the demo.

    @expose(format="json")
    def get_taxon_info6(self, taxid):
        try:
            taxid = int(taxid)
        except ValueError:
            return dict(status="error",
                        errmsg="not an integer")

        try:
            taxon = Taxonomy.get(int(taxid))
        except sqlobject.SQLObjectNotFound:
            return dict(status="error",
                        errmsg="not found")
        
        return dict(status="success",
                    id=taxon.id, species=taxon.scientific_name)

I then modified the client to show the error message if there was an error.

<script type="text/javascript">

var make_request6 = function (e) {
  /* only listen for the enter key */
  if (e.key().string != "KEY_ENTER") {
    return;
  }

  /* get the JSON from a server */
  d = loadJSONDoc("http://localhost:8080/get_taxon_info6",
                  {"taxid": e.src().value});

  d.addCallback(show_result6);
  d.addErrback(show_err6);
}

var show_result6 = function (response) {
  var result = $("result6");
  if (response.status == "error") {
     replaceChildNodes(result, "<" + response.errmsg + ">");
  } else {
    replaceChildNodes(result, response.species);
  }
}

var show_err6 = function (response) {
  var result = $("result6");
  replaceChildNodes(result, "<server failure>");
}

</script>

<div style="background: #EEE">
<input id="tax6" size="7">
<span id="result6"></span>
</div>

<script type="text/javascript">
/* Listen for the enter key; use 'onkeyup' for non-printable keys */
connect("tax6", "onkeyup", make_request6);
</script>



Copyright © 2001-2020 Andrew Dalke Scientific AB