background image
HomeRecent PostsDrupalSearchTagsRSSContactAboutAccount

Using AHAH to dynamically generate form elements (and integration with multi-tiered taxonomy)

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']

Implement on nodrefrence cck fields

in my project i implement 3 fields country state and city with node refrence now i want then as exposed filter in views as dependent how can i do this using AHAH as state is dependent on country and city is on state .

Need a little help

Hello Eric,

I need exactly what you do in your exemple.
But i don't understand all your article, and needed a little bit help !

I have a little question :

How can i use your trick into my product creation page, in order to have a user-friendly box for choose the product category ? (drupal/ubercart).

I'm starting with drupal / ubercart and don't yet understand all the vocabulary (menu callback .. ).

Thx you dude!

removing content already added...

Hey,

Cool tutorial. My only issue i'm having is with content being added, and duplicated. my scenario....

I have a set of radios, where they can select three options. When they select one of the options, what i want to do is serve underneath, further form elements relevant to the radio selection they made.

For example, the radios might say, "what page type do you want?" and then i can give these options in my radio:
- Left image with copy
- Just copy

So if they select left image, i would want to display an image upload field underneath, otherwise, just bring up body tag.

I can get that working, however, if the user selects option 1, for example, then change their mind and choose option 2 from the radio, the new form features get appended after the form elements shown from the first radio selection, hence duplicating. What i want to do is to clean out the container or form array of any elements matching what i am about to put in there.

How would i go about doing this?

I'm trying an unset on my form element just before the 'form_set_cache' call. but that doesnt work...

Any ideas?

cheers

Craig

Cant get ahah form on lightbox

Hi Eric,
Nice tutorial to learn dynamically generate forms. In my project I need same functionality on lightbox. I tried using your tutorial but cant get it on lightbox. Without lightbox its working fine. On lightbox when I click on form "select_1" other form "select_2" should be loaded but "select_2" form do not loads on lightbox but it loads on page behind lightbox. Cant understand what happens on lightbox. Any suggestions?

Thanks,
Chintan Vyas.

Thanks a lot, this was

Thanks a lot, this was incredibly useful.

One minor thing - for some reason, I couldn't get it to work with #value of my wrapper form set to &nbsp. Had to change it to an empty space. Any idea why?

Hi Eric, I made a

Hi Eric,

I made a dropdown(categories) which on selction calls #ahah and another dropdown(subcategoires) element is built. Now what i want is on selection of subcategory another ahah should be fired and artilcles should be loaded... but it is not happeningg.... tried to search alot but didnt get anything around.... Does ahah works for the element built from ahah .....

Thnks For your help in advance.

Shireen

What about Edit?

Thanks for the information.Its working for me.but what if iam editing the same form by default i need to show the selected values.Iam not depending on drupal node.

Thanks a lot for the idea and

Thanks a lot for the idea and explanation. It is difficult to imagine what it means to me. The explanation is very important to me. I'm making my first steps in it yet, but have some results. I've found a lot of books at the pdf search engine http://pdf.rapid4me.com . But they are interesting till the moment you begin to undertand. What I need now is a challenge. Thanks that you give them.

Change event

Hi,
Thanks for the tutorial.
I tried the same thing in my form_alter(). But nothing happens on change of first dropdown. Can u pls guide me for this?

Eric.London's picture

ahah

I would suggest starting here:

'#ahah' => array(
  'event' => 'change',
  'path' => 'ahah-form-callback',
  'wrapper' => 'wrapper-1',
  'method' => 'replace',
),

Have you verified that the defined path is a valid callback? Ensure that you can access it directly (example: http://MYSITE/ahah-form-callback).

Also, using a FireFox plugin like Firebug is an essential tool to see what AJAX requests are occurring, and what javascript events are bound to elements.

Three selects scenario

I have tried your snippet and it worked. Thank you very much.

No I need to do a bit more complex drop down dependency by adding one more select, a change the options of this one by selecting the second. I have added menu and ahah call back function for the second the same way as in your example, but for some reasons the second select doesn't fire on change.

Any clue why might be happening?

More than two cascading select boxes

So any one have luck with three select boxes??? To me it look like in second 'select' form #ahah doesn't respond to any event.

Eric.London's picture

firebug

I think firebug (firefox extension) might help. Can you verify that the jQuery is affecting the new form elements?

Thanks for it! There is also

Thanks for it!
There is also one thing.
If selects are #required, we have to do some append work.
Sorry for my crystal English =)

How about in form_alter and using pop-ups

Before i dive into it, thought i'd ask how easy it would be to adapt this code to a hook_form_alter and pop ups? Actually it might be nice for the select one to be a pop-up and the select 2 to be check boxes.

Example of form_alter and ahah in progress

This tutorial really helped me get started on the whole AHAH/Form scenario.
I´m posting my attempt to get it working right, and so far, so good!. I put it together with the code of this tutorial plus things I found on autocompletes.
It basically prints a selector of "parent" terms, once selected a new Autocomplete field is printed through ajax that responds only to the terms whose parent is the selected combo.

<?php
/**
* Implementation of hook_menu().
*/
function ahah_formularios_menu() {
    $items = array();
    $items['ahah_form_callback'] = array(
    'title' => 'AHAH Form Callback',
    'page callback' => '_helper_callback_ahah_form_callback',
    'type' => MENU_CALLBACK,
    'access arguments' => array('access content'),
  );
//customtaxoautocomplete
// I want only the childs of the selected parent term!!
$items['taxonomy/customtaxoautocomplete'] = array(
    'title' => 'AHAH customtaxoautocomplete',
    'page callback' => 'ahah_formularios_taxonomy_autocomplete',
    'type' => MENU_CALLBACK,
    'access arguments' => array('access content'),
  );
 
  return $items;
}


/**
* Implementation of hook_form_alter().
*/
function ahah_formularios_form_alter(&$form, &$form_state, $form_id) {

  switch ($form_id) {

    case 'borrar_node_form':
echo "<h1>BORRARRPrueba</h1>";
firep($form_id);
   // 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();
  $options[""] = "-Ninguna-";
  foreach ($tree as $key => $value) {
    $options[$value->tid] = $value->name;
  }
 
$form['select_1'] = array(
    '#type' => 'select',
    '#options' => $options,
    '#title' => t('Seleccionar tecnología principal'),
    '#size' => 1,
    '#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;',
  );
  ahah_render($form, "select_1");
     break;
  } // fin switch
}

// 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;
  }


  $form['select_2'] = array(
    '#type' => 'textfield',
    '#title' => t('Seleccionar Subcategoría'),
    '#size' => 40,
    '#autocomplete_path' => 'taxonomy/customtaxoautocomplete/'.$vid.'/'.$parentVid,
    '#maxlength' => 128,
  );

if(!is_numeric($parentVid)) {
$form['select_2']=NULL;
}
  // 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();

}



/**
* Helper function for autocompletion overriden del taxonomy module
*/
function ahah_formularios_taxonomy_autocomplete($vid,$parentTerm, $string = '') {
  // The user enters a comma-separated list of tags. We only autocomplete the last tag.
  $array = drupal_explode_tags($string);
/*echo "<script>alert('$vid.---$parentTerm')</script>";*/
  // Fetch last tag
  $last_string = trim(array_pop($array));
  $matches = array();
  if ($last_string != '') {

$result = db_query_range("SELECT t.tid,
t.name FROM {term_data} t
LEFT JOIN {term_hierarchy} term_hierarchy ON t.tid = term_hierarchy.tid
WHERE (t.vid = %d) AND (term_hierarchy.parent = %d) AND LOWER(t.name) LIKE LOWER('%%%s%%')
", $vid,$parentTerm ,$last_string, 0, 10);

    $prefix = count($array) ? implode(', ', $array) .', ' : '';

    while ($tag = db_fetch_object($result)) {
      $n = $tag->name;
      // Commas and quotes in terms are special cases, so encode 'em.
      if (strpos($tag->name, ',') !== FALSE || strpos($tag->name, '"') !== FALSE) {
        $n = '"'. str_replace('"', '""', $tag->name) .'"';
      }
      $matches[$prefix . $n] = check_plain($tag->name);
    }
  }

  drupal_json($matches);
}

// 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);
}

Eric.London's picture

hook_form_alter

Whether you create a new form or edit an existing one with hook_form_alter, it's possible!

if Validation fails ... It does gives latest added value

I have tried AHAH .. Its working !!!

But when validation error occured, newly added field is going down to the submit button.

Also for second time it gives another new control.. How to solve it?

Is it possible to multiple dropdowns based on single one

Is it possible to multiple dropdowns based on single one by using Ahah
events
i am new to drupal can you tell me possible or not

I have to same problem

Can you tell me solution this