IzziD code logo

Topics

Home

Bioinfo

Web

Misc

About

Other IzziDs

IzziDassorted

IzziDtravel

IzziDwetlab

IzziD

How to make context-sensitive forms with Ajax and Prototype

Article created: Sep 30, 2007
Article by: Jeremiah Faith

The situation

I’m a graduate student and I ran into the following challenge when dealing with microarray data. However, I’m going to translate the challenge into a car selection example, so you don’t have to be familiar with biology to understand the application (but here’s the microarray example if you want to try it out).

Let’s say we have a database that has massive amounts of information about cars (e.g. manufacturer, model, antilock brakes, year, price, color, etc…). Our goal is to allow people to use this information to narrow down a set of cars that they might want to buy (or at least research in more detail). For example, they may want a safe, affordable car for their kid, so they might want limit their car list to those with antilock brakes that are less than $10,000.

If we knew ahead of time that they were interested in price and antilock breaks, we could provide them with a form to let them fill out the relevant information (i.e. <$10,000 and must have antilock brakes). But of course, we don’t know ahead of time what they’re interested in. Let’s look at the potential solutions.

The worst solution

Put all of the form information for every possible feature on one page. Clearly this will give your user HTML-clutter overload and send them straight to a new Google search.

Old-fashioned solution

Using old-fashioned CGI and HTML, we could present them with an initial form where they choose the features they’re interested in; then, on the following page, we could present them with the relevant forms. The problem with this approach is that the user has to make lots of decisions up front. If they change their mind or want to try different things, they need hit the back button a bunch of times and start over. Nonetheless this is a decent approach that will certainly work on virtually all web browsers.

DHTML solution

But again, if you only have a dozen features or so, this DHTML solution is elegant, user-friendly, and shouldn’t be too hard to get working.

Ajax solution

Ok now for the point of the article. When you have hundreds to thousands of features, Ajax provides an efficient solution to our problem. We provide the user with a form to select features of interest to them. Once they make their selection, we retrieve, via javascript, the relevant form information for that feature. And we present the form to the user. Like the DHTML solution above, we can do this all in an elegant manner without requiring a page refresh.

Dynamic selection and update of the form elements

Our first task is to allow the user to choose the feature(s) they’re interested in. Upon making their selection, we’ll update a pane in our webpage with the relevant form. Let’s first make the html for our dynamic ajax forms page:

<html>
<head>
   <title>Ajax dynamic forms example</title>
   <link rel="stylesheet" type="text/css" href="ajax_forms.css">
   <script type="text/javascript" src="ajax_forms.js"></script>
   <script type="text/javascript" src="prototype.js"></script>
   </head>
<body>
   <form onsubmit="return check_form()">
   <input type=Submit><BR><BR>
   <table border=0 cellpadding=0 cellspacing=0 height=100%>
   <tr height=10 valign=top>
     <td>
        <select class=small name="testpulldown" size=1 
            onchange="update_div(1,this.options[this.selectedIndex].value);">
                <option value=0>List of features
                <optgroup label="General">
                <option value=1>price
                <option value=2>mileage
                <optgroup label="Exterior">
                <option value=3>color
                <option value=4>chrome wheels
                <optgroup label="Safety Features">
                <option value=5>anti-lock brakes
                </optgroup>
        </select>
     </td>
     <td>
        <select class=small name="testpulldown" size=1 
            onchange="update_div(2,this.options[this.selectedIndex].value);">
                <option value=0>List of features
                <optgroup label="General">
                <option value=1>price
                <option value=2>mileage
                <optgroup label="Exterior">
                <option value=3>color
                <option value=4>chrome wheels
                <optgroup label="Safety Features">
                <option value=5>anti-lock brakes
                </optgroup>
        </select>
    </td>
   </tr>
   <tr><td><div id=testpane1 class=testpane>Select a feature from the list</div></td>
       <td><div id=testpane2 class=testpane>Select a feature from the list</div></td>
   </tr>
  </table>
  </form>
</body>
</html>

That’s quite a bit of html, but most of it is just formatting. The heart of the html is the two <div> elements. These are our blank slates that we can update with our form elements after the user makes their feature selection. We use a pulldown <selection> form element to allow the user to select the feature they want to query. I’ve placed two <div> elements and two pulldowns, so you can see how the code works with more than one feature selection. You can modify this to any N feature selections by adding more pulldowns and more <div> elements.

We also have a small css file, ajax_forms.css, that defines the look of our page. The main thing to note here is the div.testpane, which defines the area we will subsequently fill with our form elements.

div.testpane {
   font-family:sans-serif; 
   font-size:12px; 
   background-color:#DDD; 
   width:250px; 
   height:100%; 
   padding:4px;
}
select.small {font-size:11px;}
A.stand:link { color: #2144AA; text-decoration: none; font-size:13px; } 
A.stand:active {  color: #2F3F53; text-decoration: none; font-size:13px;}  
A.stand:visited { color: #2144AA; text-decoration: none; font-size:13px;}
A.stand:hover {  color: #817D62; } 
TD.small { font-family:sans-serif; font-size:11px;}

Now we need a little javascript code…

Notice in our html <select> form element, we use onchange to call a function update_div() whenever a user makes a selection from the pulldown menu. The update_div() function’s job is to figure out what form elements we need for the requested feature and to display the relevant form for that feature. As I mentioned above, one way to do this is to use a DHTML approach where we store every possible form for all of our features. That approach would work fine here where we only have five features. But the goal of this article is to use ajax to make a more scalable solution to this problem.

For the asynchronous javascript part of our javascript code we’re going to use the prototype library. With the prototype library, it’s pretty easy to accomplish our task with very little javascript code:

function update_div(pane_id, id) {
        var pane_name = "testpane";
	var url = "/2007/Sept/Dynamic_Ajax_Forms/ajax_forms.php";
        pane_name += pane_id;
	var pars = 'feature_id=' + id + '&pane_id=' + pane_id;
	new Ajax.Updater(pane_name, url, 
		{ 
			method: 'get', 
			parameters: pars
		}
	);
}

Remember our javascript function update_div() is called from each select form element. When we call this function in our html, we pass the pane_id of our <div> element as the first variable and the user’s selection option (id) as the second variable. Our javascript function passes the user’s selection id to a php program that we will describe below. The purpose of this php program is to output the the relevant form information (in html) to the relevant <div> pane specified by pane_id. We use the prototype function Ajax.Updater() for this task.

Ajax.Updater() is an incredibly useful function in the prototype library. You pass it the name of an element in the DOM and a url, and it retrieves the data the url returns and places it into the DOM element you specify. You name these DOM elements in your html using the id tag. In our html above, we named the two <div> elements testpane1 and testpane2. We then pass these names to the Ajax.Updater() function.

Finally, we need the php code that returns the html specifying the form elements relevant for the user-selected feature.

<?php

if (isset($_REQUEST['feature_id'])) {  # feature id selected by user
   $feature_id = $_REQUEST['feature_id'];
   # this refers to where the information came from on the html page
   $pane_id = $_REQUEST['pane_id'];
   # don't draw a form for feature id 0;
   # feature id 0 occurs if the user reselects the "List of features"
   if ($feature_id == 0) {
     echo "";
     exit(0);
   }
   else {
      write_form_info($feature_id, $pane_id);
   }
}
else {
   echo "Error: you need a feature_id.";
}

function write_form_info($fid, $pid) {
   # all of our form element names will have this basename as the root
   # this way, when a form has several elements we can figure out where
   # the came from
   $basename = "testpane$pid";

   # provide a way to determine the feature id when the form is submitted
   echo "<input type=hidden name=$basename" . "feature_id value=$fid>";

   # both "price" and "mileage" are numeric
   if ($fid == 1 || $fid == 2) {
      if ($fid == 1) { echo "<i>Price</i>"; }
      else { echo "<i>Mileage</i>"; }
      # name is testpaneNmin where N is the pane_id
      echo " less than: <input type=text size=7 name=$basename" . "min>";
   }
   else if ($fid == 3) {
      echo "<i>Color</i>";
      echo " any of:<BR>";
      echo "<input type=checkbox name=$basename" . "red> red<BR>";
      echo "<input type=checkbox name=$basename" . "black> black<BR>";
      echo "<input type=checkbox name=$basename" . "green> green<BR>";
      echo "<input type=checkbox name=$basename" . "silver> silver<BR>";
   }
   else if ($fid == 4 || $fid == 5) {
      if ($fid == 4) { echo "<i>Chrome wheels</i>:<BR>"; }
      else { echo "<i>Anti-lock brakes</i>:<BR>"; }
      echo "<input type=radio name=$basename" . "decision value=yes>Yes";
      echo "<input type=radio name=$basename" . "decision value=no> No";
   }
   else {
      echo "";
   }
}
?>

Our php code retrieves the feature_id and pane_id, which were passed via GET from the Ajax.Updater asynchronous javascript function. After a few simple error checks, we call write_form_info(), which will generate the html to draw the forms. We define a basename which is mainly our pane_id. By placing this basename as the prefix to all of the names for <input> elements, any script passed the form data can group the relevant form data together. For example if the user was interested in color and selected green and red from testpane1, the form would pass testpane1red=1&testpane1green=1. The testpane1 prefix allows the receiving script to know that red and green were both selected from the same pane. While the hidden value testpane1feature_id lets the script know that the user was using the car color form (i.e. feature_id=3).

Ok, now we have group of scripts and files that allows our user to dynamically switch forms. Those forms are generated on-the-fly by our php script and are not stored and hidden in our original html. In a real situation, your php script would most likely be tied to a database with the information that it could use to determine which type of form to generate for each feature. But there are plenty of php/mysql tutorials on the web to teach you that. Also, I should mention that you don’t have to use php. The url called by Ajax.Updater() can be any valid url that returns the relevant form information.

Although we’ve used a number of different languages and technologies, we haven’t written very much code. If some of what we’ve done to this point seems difficult, it’s probably because I used a language or technology that you’re unfamiliar with. Herein lies the problem with Ajax-coding. Ajax is not a difficult language. Ajax isn’t a language at all. Ajax is simply stringing together a bunch of different technologies to improve your user’s web-experience. But to really understand Ajax and develop your own applications, you need at least a decent understanding of: html, css, dhtml, and (particularly) javascript.

Sending the form values to the car display page

For my microarray dynamic form challenge, I wrote up the code to get this far in about an hour or so. And I was quite pleased at how smoothly things went (particularly I was happy to have found the Ajax.Updater() function, which makes the trickiest part trivial). However, when I typed in some test example values into the forms and hit the submit button the unpleasant thing I’d been hoping wouldn’t happen occurred. None of the values from the asynchronously updated form elements were passed on to the script called by the form.

I spent the rest of the day trying to figure out a solution. I’m still not completely happy with the solution I came up with. It works and requires little code, but it’s certainly not very elegant (if you can think of something better, send me an email and tell me – preferably with some sample code: bfjf@yahoo.com).

After messing around for a good while, I figured out that the asynchronously generated form information was available to javascript, it just wasn’t being passed by the browser to the destination script. So I decided to run a javascript function check_form() on_submit. The check_form function generates a GET query and uses a redirect to send the GET query to the destination script.

/* this function is because the ajax form elements seem to only 
   be visible to javascript and not the browser. the browser 
   won't pass on my stinkin parameters, so I'm passing them through
   a javascript redirect.  if anyone knows a better way, I'd be 
   happy to hear it!!
*/
function check_form() {
   // grab all of the input elements
   var x = document.getElementsByTagName('input');
   var GET_info = "";

   for (var i=0;i<x.length;i++) {
      // skip unchecked checkboxes
      if ((x[i].type == 'checkbox' || x[i].type == 'radio') && !x[i].checked) {
         continue;
      }

      if (i == 0) { GET_info = x[i].name + '=' + x[i].value; }
      else { GET_info +=  "&" + x[i].name + "=" + x[i].value; }
   }
   this.document.location.href='final_script.php?' + GET_info;

   return false;  // is there no better way????????
}

For completeness, we need one final script. In a real world scenario, this final script would take the parameters passed from our form and present the appropriate list of cars. Here our script is just going to display all the variables passed by the GET url.

<?php
   echo "Variables passed to this form:<BR>";
   foreach ($_GET as $key => $value) {
      echo "$key => $value<br>\n";
   }
?>

Here is the working dynamic forms example and the final dynamic forms javascript code.