background image
HomeRecent PostsDrupalSearchTagsRSSContactAboutAccount
Eric.London's picture

For some time now I've wanted to write a blog entry about using AHAH to create dynamically generated form elements. After a recent conversation at work regarding usability, I now had a real world example to create: how to use tiered taxonomy to dynamically generate a form. This code snippet will show you how to create a form that creates child select dropdowns based on the parent taxonomy term the user selects.

First I established a multi-tier taxonomy called "AHAH":

For this example I created a menu callback to display my initial form:

<?php
function helper_menu() {
 
 
$items = array();
 
 
$items['ahah-form'] = array(
   
'title' => 'AHAH Form',
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('_helper_callback_ahah_form'),
   
'type' => MENU_CALLBACK,
   
'access callback' => 'user_access',
   
'access arguments' => array('access content'),
  );
 
  return
$items;
 
}
?>

I then defined the page callback to show the initial form:

<?php
function _helper_callback_ahah_form() {

 
// define an array to contain form elements
 
$form = array();
 
 
// define the top level vid
 
$vid = 2;
 
 
// fetch a tree of taxonomy elements
 
$tree = taxonomy_get_tree($vid, 0, -1, 1);
 
 
// loop though taxonomy and collect elements
 
$options = array();
  foreach (
$tree as $key => $value) {
   
$options[$value->tid] = $value->name;
  }
 
 
// create the first select dropdown input
 
$form['select_1'] = array(
   
'#type' => 'select',
   
'#options' => $options,
   
'#title' => t('Select 1'),
   
'#size' => 5,
   
'#multiple' => false,
   
'#ahah' => array(
     
'event' => 'change',
     
'path' => 'ahah-form-callback',
     
'wrapper' => 'wrapper-1',
     
'method' => 'replace',
    ),
  );
 
 
// pass the top level vid in the form
 
$form['ahah_vid'] = array(
   
'#type' => 'hidden',
   
'#value' => $vid,
  );
 
 
// create an empty form element to contain the second taxonomy dropdown
 
$form['wrapper_1'] = array(
   
'#prefix' => '<div id="wrapper-1">',
   
'#suffix' => '</div>',
   
'#value' => '&nbsp;',
  );
 
 
// add a form submit button
 
$form['submit'] = array(
   
'#value' => 'Submit',
   
'#type' => 'submit'
 
);
 
  return
$form;
 
}
?>

The above form callback produces the following:

Next, I defined a callback to handle the AHAH page request:

<?php
// new menu item:
function helper_menu() {
 
 
// ...
 
 
$items['ahah-form-callback'] = array(
   
'title' => 'AHAH Form Callback',
   
'page callback' => '_helper_callback_ahah_form_callback',
   
'type' => MENU_CALLBACK,
   
'access callback' => 'user_access',
   
'access arguments' => array('access content'),
  );
 
 
// ...

 
return $items;
 
}

// and, here's the AHAH callback used to create the new form elements:
function _helper_callback_ahah_form_callback() {
 
 
// define a string variable to contain callback output
 
$output = "";
 
 
// pull the top level vid from the $_POST data
 
$vid = $_POST['ahah_vid'];
 
 
// pull the selected dropdown from the $_PODT data
 
$parentVid = $_POST['select_1'];
 
 
// loop through the taxonomy tree and fetch child taxonomies
 
$options = array();
 
$tree = taxonomy_get_tree($vid, $parentVid, -1, 1);  
  foreach (
$tree as $key => $value) {
   
$options[$value->tid] = $value->name;
  }
 
 
// define the second tier select dropdown element
 
$form['select_2'] = array(
   
'#type' => 'select',
   
'#options' => $options,
   
'#title' => t('Select 2'),
   
'#size' => 5,
   
'#multiple' => false,
  );
 
 
// rebuild form object and output new form elements
 
$output .= ahah_render($form, 'select_2');
 
 
// render form output as JSON
 
print drupal_to_js(array('data' => $output, 'status' => true));
 
 
// exit to avoid rendering the theme layer
 
exit();
 
}

// Lastly, here's a help function pulled from Nick Lewis's blog to alter the form
// see: http://www.nicklewis.org/node/967
// NOTE: based on poll module, see: poll_choice_js() function in poll.module
function ahah_render($fields, $name) {
 
$form_state = array('submitted' => FALSE);
 
$form_build_id = $_POST['form_build_id'];
 
// Add the new element to the stored form. Without adding the element to the
  // form, Drupal is not aware of this new elements existence and will not
  // process it. We retreive the cached form, add the element, and resave.
 
$form = form_get_cache($form_build_id, $form_state);
 
$form[$name] = $fields;
 
form_set_cache($form_build_id, $form, $form_state);
 
$form += array(
   
'#post' => $_POST,
   
'#programmed' => FALSE,
  );
 
// Rebuild the form.
 
$form = form_builder($_POST['form_id'], $form, $form_state);

 
// Render the new output.
 
$new_form = $form[$name];
  return
drupal_render($new_form);
}
?>

The above code allows the user to select an option from the top level tier of taxonomy and the AHAH callback will generate the a select dropdown of the child taxonomies as shown below:

On form submission, you'll see that the options the user selected as stored in $form_state['values']['select_1'] and $form_state['values']['select_2']

In this tutorial, I'll show you how you can expose your search form on another site using jQuery. At first, I thought about scraping the form's html using AJAX.. and quickly remembered you cannot easily do that. Which lead me to review the AJAX functionality included in jQuery. Bingo, one of my favorites: jQuery.getJSON. To summarize this code, I create a callback function to display the form's json-ified html which can then be easily embedded on another site.

First I defined the menu hook:

<?php
function MYMODULE_menu() {

 
$items = array();

 
// add a page callback for the url: "external-search.js"
 
$items['external-search.js'] = array(
   
'page callback' => '_MYMODULE_external_search',
   
'type' => MENU_CALLBACK,
   
'access arguments' => array('search content'),
  );
   
  return
$items;
   
}
?>

Then I created the callback function for the menu callback:

<?php
function _MYMODULE_external_search() {

 
// create a json string of the search form html
 
$json = drupal_to_js(drupal_get_form('search_form'));
   
 
// format the json as a callback function
  // see: http://docs.jquery.com/Ajax/jQuery.getJSON for more information
 
if ($_GET['jsoncallback']) {
   
$json = $_GET['jsoncallback'] . "(" . $json . ");";
  }
   
 
// output the json
 
print $json;

 
// stop the script, so the theme layer is not applied
 
die;
}
?>

One problem though, the form submits locally. That can be fixed using a form_alter function:

<?php
function MYMODULE_form_alter(&$form, $form_state, $form_id) {
   
 
// check for external search form and set form action to be full path
 
if ($form_id == 'search_form' && arg(0)=='external-search.js') {
   
// change the form action to be the full path
   
$form['#action'] = 'http://' . $_SERVER['HTTP_HOST'] . $form['#action'];
  }
}
?>

Now, if you clear your cache and go to http://YOURSITE/external-search.js, you should see the JSON (and nothing else).

Lastly, you can embed the code on another site using a few lines of jQuery. You can even pull the jQuery from your site if the external site does not have jQuery included.

<!-- Include jQuery (as necessary) -->
<script type='text/javascript' src='http://YOURSITE/misc/jquery.js' ></script>

<!-- create a div container to contain the search form -->
<div id='embedded_search'></div>

<!-- add the jQuery to embed the form -->
<script type='text/javascript'>
$(document).ready(function(){
  // make the ajax request
  $.getJSON("http://YOURSITE/external-search.js?jsoncallback=?",
    function(data){
      // append the form to the container
      $('#embedded_search').append(data);           
    }
  );
});
</script>

Now people should be able to access your site's search form from another site!

In this snippet, I'll explain how to implement an autocompleting field using the forms API and a menu callback. The first thing you'll need to create is the callback function that queries the database and returns an non-themed javascript result. In this function I'll return a list of node IDs and their titles:

<?php
function MYMODULE_autocomplete_node($userString) {
 
// create the SQL to query the node table
 
$sql = "select nid, title from {node} where status='1' and lower(title) like lower('%%%s%%') order by title asc";

 
// query the database
 
$resource = db_query_range($sql, $userString, 0, 10);

 
// loop through the results and create an associative array
 
$results = array();
  while (
$row = db_fetch_array($resource)) $results[$row['nid']] = $row['title'];

 
// output the results in javascript
 
print drupal_to_js($results);

 
// exit, to prevent your results form hitting the theme layer
 
exit();
}
?>

Next, create a menu item...

<?php
function MYMODULE_menu() {
 
$items[] = array();
 
// ...code...
 
$items['MYMODULE/autocomplete/node'] = array(
   
'type' => MENU_CALLBACK,
   
'access arguments' => array('access content'),
   
'page callback' => 'MYMODULE_autocomplete_node',
  );
 
// ...code...
 
return $items;
?>

Now, if you flush your menu cache and go to your newly created menu path, you should see a list of nodeID and their titles formatted in JSON. Lastly, you'll need to create the form element:

<?php
function MYMODULE_form() {
 
$form = array();
 
// ...code...
 
$form['MYFIELDNAME'] = array(
   
'#type' => 'textfield',
   
'#title' => t('MYFIELDTITLE'),
   
'#autocomplete_path' => 'MYMODULE/autocomplete/node',
  );
 
// ...code...
 
return $form;
}
?>

The purpose of this code snippet is to show you how you can use the lightmodal functionality in the Lightbox2 module to embed a form in a fancy css popup window. But why would you want to do this? Because it looks great and it improves usability.

The first thing you'll need to do is register a menu item for the page callback that you'd like to appear in the lightbox window.

<?php
function MYMODULE_menu() {
 
$items[] = array();

 
$items['MY/URL'] = array(
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('MYCALLBACK_form'),
   
'type' => MENU_CALLBACK,
   
'title' => t('MY TITLE')
  );

  return
$items;
}
?>

Next, define a function to create the form

<?php
function MYCALLBACK_form($form_state) {
 
$form = array();

 
// add your form elements here. for example:
 
$form['MYTEXTFIELD'] = array(
   
'#type' => 'textfield',
   
'#title' => t('MY TEXT FIELD'),
  );

 
// add submit button
 
$form['submit'] = array(
   
'#value' => 'Submit',
   
'#type' => 'button'
 
);

 
// add cancel button
  // NOTE: the cancel button simply closes the lightbox window
 
$form['cancel'] = array(
   
'#value' => 'Cancel',
   
'#type' => 'button',
   
'#attributes' => array(
     
'onClick' => "Lightbox.end('forceClose'); return false;"
   
)
  );

  return
$form;
}
?>

Next, add a link to your page content to open up the lightbox window with your new form in it. By adding the rel=lightmodal property onto an A tag, you'll automatically load the contents of the href in a fancy lightbox window.

<?php
// NOTE: you should use the l() function here to properly generate your A tag
$page_contents .= '<a href="/MY/URL" rel="lightmodal">MY LINK TEXT</a>';
?>

I made my lightbox form more usable by using ajax to submit the form to another page callback. First, I had to add an onSubmit javascript event handler to my form to prevent it from submitting.

<?php
function MYCALLBACK_form($form_state) {
 
// ...code...
 
$form['#attributes'] = array(
   
'onsubmit' => 'return my_js_submit_function(this);'
 
);
 
// ...code...
}
?>

Now, define a javascript function to validate and submit the form using AJAX.

function my_js_submit_function(whichThis) {
  // validate your form here

  // define arguments to pass to ajax page callback
  // you'll actually want to collect the user submitted form data here
  var args = {
    myArg1: 'blah',
    myArg2: 'blah',
  };

  // submit the data using jQuery
  // in this case, I'm using the getJSON function which uses GET and expects a JSON object in return
  $.getJSON('/MY/URL/SUBMIT', args,
    function(json){
      // check for a return status, show a message, close the lightbox window, etc
      alert(json.message);
      if (json.status == true) {
        Lightbox.end('forceClose');
      }
    }
  );
}

You'll need a final page callback and menu item to process the AJAX

<?php
function MYMODULE_menu() {
 
// ...code...
 
$items['MY/URL/SUBMIT'] = array(
   
'page callback' => 'MYMODULE_CALLBACK_SUBMIT',
   
'type' => MENU_CALLBACK,
  );
 
// ...code...
}

function
MYMODULE_CALLBACK_SUBMIT() {
 
// 1. validate $_GET (not shown, use your imagination)
  // 2. process $_GET (not shown, use your imagination)
  // 3. return a JSON object

  // create return object
 
$returnO = new StdClass();
 
$returnO->status = true;
 
$returnO->message = 'MY INFORMATIVE MESSAGE HERE';

 
// output json result
 
print drupal_json($returnO);

 
// NOTE: if you don't die here, then the theme will be processed. we only want to return a JSON object
 
die;
}
?>

In the end, what we have is a link that opens up a lightbox window that contains a form that is submitted via ajax.

My last thoughts... because the lightmodal functionality loads the URL from the href in the A tag, you might want to create a template file for your page layout that is simplified to prevent your normal page layout from being rendered. For instance, you could create a theme file called, page-MY-URL.tpl.php that contains the following:

<?php
if ($title) print "<h1>$title</h1>";
if (
$messages) print $messages;
if (
$help) print $help;
if (
$content) print $content;
?>

Syndicate content