Dalke Scientific Software: More science. Less time. Products

Ajax example

I decided to show a more complex Ajax example. As you type something into the search form the javascript does the search right away on the server to give results. Note that the system degrades gracefully - if Javascript isn't enabled then the form works like a regular search command.

Here's the template for the page. There's nothing special in the body except for the div element where the results will go. The new code is in the top, where the script tag first pulls in MochiKit and then pulls in special code for the form.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
    py:extends="'master.kid'">

<head>
    <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
    <title>AJAX Taxonomy search page</title>
    <script type="text/javascript" src="tg_static/js/MochiKit.js"></script>
    <script type="text/javascript" src="static/javascript/tax_search.js"></script>
</head>

<body>
<form method="GET" action="results">
Tax id: <input type="text" id="query" name="query"></input>
</form>

<div id="quick_results"></div>
</body>
</html>

The code describing how everything goes together is in tax_search.js. It's in the "static/javascript" subdirectory. The "static" directory is the default directory and URL in TurboGears for images, sound files, movies, etc. - things that don't change and aren't affected by any Python code. "javascript" is the default subdirectory for Javascript code. Notice also that TurboGears reserves "/tg_static" for the static files of the TurboGears installation. Mochikit is until the "js" subdirectory of that. Sadly it uses two different names ("js" and "javascript") for the same content type.

/* Keep track of previous requests */
/* When will there be a duplicate request?  After a backspace. */
var cached_results = {};
var currently_displayed_result = {query: "", msg:
				  "no search results",
				  taxons: []};

var format_row = function (taxon) {
  var url = "/taxon/" + taxon.tax_id;
  return TR(null,
	    TD(null, A({href: url}, taxon.tax_id)),
	    TD(null, A({href: url}, taxon.scientific_name)));

};



var show_results = function (result) {
  currently_displayed_result = result;
  replaceChildNodes($("quick_results"), result.msg);
  if (result.taxons.length != 0) {
    appendChildNodes($("quick_results"),
		     TABLE({border: 1},
			   THEAD(null, TR(null,
					  TD(null, "id"), TD(null, "name"))),
			   TBODY(null, map(format_row, result.taxons)))
		     );
  }
};

var handle_result = function (result) {
  /* duplicate response; ignore it */
  if (result.query in cached_results) {
    return;
  }

  /* cache got too large; clear it */
  if (cached_results.length > 50) {
    cached_results = {};
  }
  /* add response to cache */
  cached_results[result.query] = result;

  /* check if I need to update the view. The query must match the  */
  /* current query text and must not match the current result. */
  if (result.query == $("query").value &&
      result.query != currently_displayed_result.query) {
    show_results(result);
  }
};

var make_quick_query = function () {
  var query = $("query").value;

  /* Is the answer already displayed? */
  if (query == currently_displayed_result.query) {
    /* no need to do anything */
    return;
  }
  /* Is the answer already in cache? */
  if (query in cached_results) {
    result = cached_results[query];
    show_results(result);
    return;
  }
  
  /* Otherwise, look it up on the server */
  var d = loadJSONDoc("quick_search", {"query": $("query").value});
  d.addCallback(handle_result);
};


/* Called when there's a likely change in the text input */
/* Delay the request slightly so the browser isn't making */
/* a request for every fast keystroke. */
/* This also simplifies the logic of figuring out if the */
/* key event will change the input text. */
var timer = null;
var start_timer = function (e) {
  /* no need to finish with an existing timer */
  if (timer) {
    timer.cancel();
  }
  timer = callLater(0.05, make_quick_query);
};

var main = function() {
  /* connect everything together */
  connect($("query"), "onkeypress", start_timer);
  connect($("query"), "onkeyup", start_timer);

  /* Set mouse focus on the input element */
  $("query").focus()
};

/* call 'main' after the document is fully loaded */
addLoadEvent(main);

Finally, here are the new controller methods to handle the interface.

    # Return the search form
    @expose(template="taxonomyserver.templates.tax_search")
    def tax_search(self):
        return {}

    # Implement the form interface by forwarding taxid
    # queries (integers) and search terms (everything else)
    # to existing interfaces.
    @expose()
    def results(self, query):
        try:
            tax_id = int(query)
        except ValueError:
            # not an integer; redirect to the existing
            # text search page
            redirect("search", text=query, searchtype="contains")
        else:
            # Is a tax id; redirect to the detail page
            redirect("taxon/%d" % tax_id)

    # Called by the Javascript/Ajax code
    @expose(format="json")
    def quick_search(self, query):
        try:
            taxid = int(query)
        except ValueError:
            # text query
            taxons = list(Taxonomy.select(
                Taxonomy.q.scientific_name.contains(query),
                orderBy = Taxonomy.q.id)[:21])
            if len(taxons) == 0:
                msg = "no records matched"
            elif len(taxons) == 1:
                msg = "1 record matched"
            elif len(taxons) == 21:
                msg = "showing first 20 matching records"
                taxons = taxons[:20]
            else:
                msg = "showing first %d matching records" % (len(taxons),)
        else:
            # look up by id
            try:
                taxons = [Taxonomy.get(taxid)]
                msg = "taxon id found"
            except sqlobject.SQLObjectNotFound:
                # no match
                taxons = []
                msg = "no entry matches the taxon id"

        return {"query": query,
                "msg": msg,
                "taxons": [{"tax_id": taxon.id,
                            "scientific_name": taxon.scientific_name}
                                for taxon in taxons]}



Copyright © 2001-2013 Andrew Dalke Scientific AB