Web Development Blog

Learning Drupal Module Dev: Dependent AHAH/AJAX Forms

03.21.2010 11:32PM by Evan Johnson

So, I've been working on a simple module for a Drupal project lately. I wanted to have two administrator settings for the module, the second of which depends on the first. A "dependent dropdown". It would be cool if it was "AJAXy" too, I thought. Well, it just so turns out that in Drupal 6 there are built in AHAH (Asychronous HTML and HTTP) functions in the Forms API to do just this. Easy! Or is it?

It also turns out there is a steep learning curve to making "Ajax forms" with the Drupal Forms API. I got it working, but it took a fair amount of effort. So, to help out future Drupal AHAH developers I am providing my code below, along with a list of links to resources that were a great help in unraveling this problem.

First, to help provide an "aerial view" of what's going on here, this is a list of the components involved:

  • A form that has the AHAH fields (ahahtestmodule_admin_settings in this example)
  • The first field in the form (ahahtestmodule_types), which will change the contents of the second field
  • The second, "dependent" field (ahahtestmodule_ahah_field)
  • The function that provides the options of the AHAH field based on the first field (ahahtestmodule_get_ahah_fields())
  • The AHAH function that updates the AHAH field (ahahtestmodule_ahah_field_js())
  • menu callback function for the AHAH function (ahahtestmodule/ahahjs)

To start out, here is the example admin settings form ahahtestmodule_admin_settings with both fields (ahahtestmodule_types and ahahtestmodule_ahah_field):

<?php 
function ahahtestmodule_admin_settings() {
  $form = array();
  $form['settings'] = array(
    '#type' => 'fieldset',
    '#title' => t('ahahtestmodule Settings'),
  );
  $form['settings']['ahahtestmodule_types'] = array(
    '#type' => 'radios',
    '#title' => t('First Field'),
    '#description' => t('Change this field to change the options in the next field.'),
    '#options' => array('one' => t('Option 1'), 'two' => t('Option 2'), 'three' => t('Option 3')),
    '#default_value' => variable_get('ahahtestmodule_types', 'one'),
    '#ahah' => array(
      'path' => 'ahahtestmodule/ahahjs',
      'wrapper' => 'ahah-wrapper',
      'method' => 'replace',
    ),
  );
  $form['settings']['ahahtestmodule_ahah_field'] = array(
    '#type' => 'select',
    '#title' => t('Dependent Second Field'),
    '#options' => ahahtestmodule_get_ahah_fields(variable_get('ahahtestmodule_types', 'one')),
    '#default_value' => variable_get('ahahtestmodule_ahah_field', 'none'),
    '#description' => t('This fields content depends on what is selected in the first field.'),
    '#prefix' => '<div id="ahah-wrapper">',
    '#suffix' => '</div>',
  );
  return system_settings_form($form);
}
?>

Next, here is the dummy function that gets the right content for ahahtestmodule_ahah_field based on ahahtestmodule_types:

<?php 
function ahahtestmodule_get_ahah_fields($first_variable) {
  $ahah_fields = array();
  switch ($first_variable) {
    case 'one':
        $ahah_fields['one'] = 'Option 1 Was Selected';
        break;
    case 'two':
        $ahah_fields['two'] = 'Option 2 Was Selected';
        $ahah_fields['two_bonus'] = 'Bonus Option!';
        break;
    case 'three':
        $ahah_fields['three'] = 'Option 3 Was Selected';
        break;
    default:
        $ahah_fields['none'] = 'Please Select...';
  }
  return $ahah_fields;
}
?>

Then, here is the magic AHAH callback function that I don't fully understand and ripped right off this article at drupal.org: Adding dynamic form elements using AHAH:

<?php 
// The AHAH callback function
function ahahtestmodule_ahah_field_js() {
  // The AHAH callback function triggered by the user changing the first field, "ahahtestmodule_types"
  $form_state = array('storage' => NULL, 'submitted' => FALSE);
  $form_build_id = $_POST['form_build_id'];
  // Get for the form from the cache
  $form = form_get_cache($form_build_id, $form_state);
  // Get the form set up to process
  $args = $form['#parameters'];
  $form_id = array_shift($args);
  $form_state['post'] = $form['#post'] = $_POST;
  $form['#programmed'] = $form['#redirect'] = FALSE;
  // Process the form with drupal_process_form(), which calls the submit handlers that put whatever was worthy of keeping in the $form_state
  drupal_process_form($form_id, $form, $form_state);
  // Call drupal_rebuild_form(), which destroys $_POST, creates the form again with hook_form, gets the new form cached and processed again
  $form = drupal_rebuild_form($form_id, $form_state, $args, $form_build_id);
  // THIS IS WHAT YOU WILL CUSTOMIZE FOR YOUR OWN FORM
  // Choose the field you want to update with AHAH and render it
  $ahah_form = $form['settings']['ahahtestmodule_ahah_field'];
  unset($ahah_form['#prefix'], $ahah_form['#suffix']);
  $output = drupal_render($ahah_form);
  // Final rendering callback.
  drupal_json(array('status' => TRUE, 'data' => $output));
}
?>

Lastly, be sure to add the menu callback for ahahtestmodule_ahah_field_js():

<?php 
function ahahtestmodule_menu() {
  $items = array();
  $items['ahahtestmodule/ahahjs'] = array(
    'page callback' => 'ahahtestmodule_ahah_field_js',
    'access arguments' => array('administer ahahtestmodule'),
    'type' => MENU_CALLBACK,
  );
  return $items;
}
?>

I rolled this whole thing up into a little demo module that does nothing except run all this code:

Download the Drupal Ahah Test Module

Here are some links that I used to figure this out that will hopefully help you too:

A big thanks to the Drupal community as always for putting so much helpful support up online for free!

Comment

Gravatar Image
On Apr 20, 01:35 PM, (Link)
Geoff Maxey (cntlscrut) said:

I noticed you referred to my article, "An Adventure With AHAH and the Drupal Form API."
Cheers for that.

I would like to recommend that anybody interested would want to also read this article:
http://maxeydevbox.org/blogs/geoffmaxey/building-custom-compound-fieldswidgets-cck-drupal-6x

That article and code goes a bit deeper into the subject and all of the code posted has been tested.

Thanks!

Gravatar Image
On Jul 6, 10:16 AM, (Link)
Scott (scottalan) said:

I’m new to the form API. Could you please elaborate on where the 'path' comes from?

'path' => 'ahahtestmodule/ahahjs',
(ahahjs, is what I’m curious about)

I’ve used your ahahtestmodule as a reference for building a form and it seems to work up until I make my first selection. The second 'dependent' dropdown then vanishes. I’m only assuming that this has to do with the 'path', it is the only line I don’t seem to understand.

Thank you for a great tutorial!

Gravatar Image
On Jul 6, 11:03 AM, (Link)
Evan Johnson said:

@Scott – The #ahah 'path' attribute in the form is the URL of the callback function you declared in ahahtestmodule_menu(). We want the AHAH to call and print out the return value of the ahahtestmodule_ahah_field_js function. We do this by mapping that function to the 'ahahtestmodule/ahahjs' URL in ahahtestmodule_menu() override function (an implementation of hook_menu()).

If your second dropdown is vanishing that probably means the function the 'ahahtestmodule/ahahjs' URL is calling is returning NULL or an error instead of the dependent dropdown, so check that it’s mapped correctly in the _menu() override.

Gravatar Image
On Jul 13, 08:52 AM, (Link)
Jeff said:

For some reason, when I add this to a moderately complex form I have (about 20 fields), the rebuilt $form doesn’t have any field definitions in it, only the (posted) values. I’ve tried un-tree-ing it (it uses 3 fieldsets with identical fields, so I renamed the fields in the 3 sets so they’re different), but that (rather large) effort didn’t make any difference. I’m really getting discouraged; the whole thing seems so opaque.

Do I need to throw the whole thing out and try another approach entirely, or is there any way to tell what has gone wrong (presumably because I made some error)?

  Subscribe to article comments