AHAH in Drupal: may it one day live up to its acronym

With a name like AHAH, one might expect positive experiences in one's dealings with it. But often a name like "AGAH!" would seem more appropriate (Asynchronous Groaning and Headbashing?). There's no doubt about it - AHAH in Drupal is hard. I'm referring here to the trick of dynamically changing elements on a form or adding new ones, as is done on the poll creation form in core. It was next to impossible in Drupal 5, promises to be fairly straight-forward in Drupal 7, but has many people tearing their hair out in Drupal 6.

In a nutshell, the reason it's hard is that Drupal needs to ensure that all form submissions are legal and secure, so it won't simply accept submissions from elements it didn't know about when it first rendered the form.

A few months ago, I wrote a blog post entitled The dual aspect of Drupal forms and what this means for your AHAH callback, after having been enlightened by chx as to Form API / AHAH best practices. It focused on explaining the correct way to do AHAH in Drupal 6, as opposed to the "old way", which at the time was still the most common. Poll module, many people's starting point for understanding AHAH in Drupal, was still doing it the "old way" but has since been updated. Dmitrig01, chx and I put together a handbook page entitled Doing AHAH correctly in Drupal 6 and Beyond, intended as the official instruction guide for learning AHAH.

But there had already been a fair amount of documentation written around the older, incorrect technique which had shaped people's conceptualisation of how AHAH should work, and so a lot of confusion and frustration ensued. The new way requires quite a shift in thinking regarding what the AHAH callback is all about. It can be hard to see it just looking at the code, but here essentially is the difference between the two methodologies:

The old way
You attach an #ahah binding to a form element which includes a path to an AHAH callback function, which changes a portion of your form and returns that changed portion.

The correct way
You attach an #ahah binding to a form element which includes a path to an AHAH callback function. That function processes the form, including calling the submit handler for the element, which updates $form_state, then rebuilds it and returns a specified portion.

The big difference here is that in the second approach, the AHAH callback does not change the form. That is not its purpose. Failure to understand this is, I believe, the main source of all the confusion.

I have found that one of the most common use cases for AHAH in Drupal (going by discussions in irc and the forums) is the idea of a dependent drop-down: the options available in a dropdown are dependent on the user's input in another field. In this post I will show how to do this using the recommended technique, in an attempt to convince readers that once you embrace this new way of thinking about AHAH, it doesn't need to be so frustrating.

The form I use in my example is a "musician signup" form, which has a dropdown for "instrument category", containing the options "brass", "strings", "woodwind" and "percussion". Then there's an "instrument" dropdown, the options of which change according to which category has been chosen. It's a pretty over-simplified example - the options are coming from a 2-dimensional array that's just returned from a function, whereas in most cases they'll probably be pulled from the database. I wanted to pare the example down to the bear basics so as to focus on the AHAH essentials.

It's not about magic
You need to stop thinking that you just build your form the same old way and then AHAH will work some magic on it and change it. It may look like magic to the user, but it shouldn't feel like magic to you, the developer. As you build your form function, you need to think of all the possible ways it could look to the end user: in our case we can say that it will have an instrument dropdown that will either contain the instrument options corresponding to the chosen category, or will not have any options yet if no category has been chosen. Let's get started...

<?php
/**
* Musician signup form definition
*/
function musician_example_form($form_state, $musician = array()) {
 
$form = array();
 
$form['#cache'] = TRUE;

 
// the contents of $musician will either come from the db or from $form_state
 
if (isset($form_state['musician'])) {
   
$musician = $form_state['musician'] + (array)$musician;
  }

 
// ... element definitions

}
?>

The two important things to note here:

  1. We make sure our form is going to be cached so it can be retrieved by our AHAH callback
  2. We make it react to $form_state. The musician array could be populated from the database, if this is an edit form, or it could be populated from $form_state['musician'] which gets set by the submit handler of our #ahah element and assigned the contents of $form_state['values']. We will see this in action further on.
For the purposes of discussing this example we will always assume that $musician, if not empty, has been populated from $form_state, and not from the database as this is not an edit form.

Now for the dependent dropdowns logic:

<?php

function musician_example_form($form_state, $musician = array()) {
 
// ... form logic

  // retrieve our array of arrays of instruments, keyed
  // by category
 
$instruments = _get_instruments();
 
$instrument_categories = array_keys($instruments);
 
// format the array into an array of dropdown options
 
$instrument_options = array();
  foreach(
$instrument_categories as $key => $value) {
   
$instrument_options[$value] = $value;
  }
 
// if our $musician array (which came from $form_state) has an
  // instrument category value, we'll use that as the default value
 
$selected_category = isset($musician['instrument_category']) ? $musician['instrument_category'] : 'none';

 
// ... element definitions

}
?>
The comments hopefully make this code pretty clear - the main thing is that we now have a variable, $selected_category, which will act as both a #default_value for our category dropdown and the basis for building our instrument dropdown. If $musician['instrument_category'] is set, that means it came from $form_state when the user chose a category.

Here is the code for the dropdowns:

<?php
  $form
['instrument_category'] = array(
   
'#type' => 'select',
   
'#title' => t('Instrument category'),
   
'#options' => array('none' => 'Please select...') + $instrument_options,
   
'#default_value' => $selected_category,
   
'#ahah' => array(
     
'path' => 'musicianform/ahah',
     
'wrapper' => 'instrument-ahah',
     
'event' => 'change',
    ),
  );
 
$form['instrument_select'] = array(
   
'#tree' => TRUE,
   
'#prefix' => '<div id="instrument-ahah">',
   
'#suffix' => '</div>',
  );

 
$form['instrument_select']['instrument'] = array(
   
'#type' => 'select',
   
'#title' => t('Instrument'),
   
'#options' => $selected_category == 'none' ? array() : $instruments[$selected_category],
  );
?>
Here we add our #ahah binding to the category dropdown, telling it the path to our AHAH callback, the id of the wrapper div constituting the section of the form we are going to replace, and the event that we want to trigger the callback, i.e. on change. We then create our wrapper div for the instrument dropdown and then the dropdown itself. The array of options for the dropdown will be empty if $selected_category is 'none', otherwise it will pull the array of instruments from the $selected_category key of the $instruments array. We will be replacing the instruments dropdown with a newly rendered version each time the user changes the category.

And now for the slightly awkward bit. We need a submit handler that's specific to our #ahah element which will make the required change to $form_state. But only buttons have submit handlers and our #ahah element is a dropdown. So what we need is to add a submit button, which we'll then have to hide with css, and set our submit handler function as the #submit property of this button.

Here's the button code:

<?php
  $form
['get_instruments'] = array(
   
'#type' => 'submit',
   
'#value' => 'submit_category',
   
'#submit' => array('musicianform_get_instruments_submit'),
  );
?>
And here's the submit handler function:
<?php
/**
* Submit handler for the Instrument category drop down.
*/
function musicianform_get_instruments_submit($form, &$form_state) {
 
$musician = $form_state['values'];
  unset(
$form_state['submit_handlers']);
 
form_execute_handlers('submit', $form, $form_state);
 
$form_state['musician'] = $musician;
 
$form_state['rebuild'] = TRUE;
}
?>
This ensures that none of the other submit handlers get called and that $form_state['musician'] now contains the values submitted in the form.

And now, finally, to the AHAH callback. This is a standard sequence of steps, which are explained in detail in Doing AHAH correctly in Drupal 6. The only change you'll need to make in your own callback is in the part at the end that renders the new elements:

<?php
/**
* ahah callback.
*/
function musicianform_ahah() {

 
// ... [steps to retrieve, process and rebuild the form] ...
  // we now have a $form variable containing the
  // rebuilt form

 
$changed_elements = $form['instrument_select'];
  unset(
$changed_elements['#prefix'], $changed_elements['#suffix']); // Prevent duplicate wrappers.
 
drupal_json(array(
   
'status'   => TRUE,
   
'data'     => theme('status_messages') . drupal_render($changed_elements),
  ));
}
?>
Here we are choosing the portion of the form that needs to get re-rendered - essentially just the instruments dropdown and its parent, stripping out the wrapper div. We then call drupal_json() which prints the response in the format expected on the JavaScript side, where it will get inserted into the DOM, replacing the old version.

The fact that the bulk of the code in the AHAH callback is always exactly the same shows that this of course should be a utility function in Drupal. And indeed it is... in Drupal 7. In the meantime, you just need to copy and paste it ;-)

I suppose finishing off with "And that's all there is to it!" wouldn't be quite appropriate here - there are quite a few steps. But once you understand the rationale behind doing it this way (which is explained in my earlier post, The dual aspect of Drupal forms and what this means for your AHAH callback, and get your head around the technique itself, you will not only have an easier time of it in Drupal 6 but you will absolutely sail through all things AHAH in Drupal 7. To see why, here are some of the important changes that have either already been committed or are being worked on:

Full example

- Can you provide a full example, without skipping hook_menu and the other parts of code you skipped.
- Also add a text field besides the two selects and make it required.
- As a PHP developer new to Drupal I find this technology very hard to find about.

Thank you for the nice post, but also please consider Drupal illiterate people such as myself!

Binding the get_instruments button to the onchange event

I understood the part about the hidden submit button that will only trigger the submit handler for changing the dropdown. I am a bit confused about how the onchange event of the dropdown triggers the submit action of the hidden submit button.

Thanks
Anoop

To be honest I'm a bit

To be honest I'm a bit confused about that part myself but here's what I think is the explanation: there is no explicit association between the dropdown and the hidden button, it's just that that button's submit handler is the first one that gets called because of where the button is on the form, and it makes sure the other submit handlers don't get called.

Nearest submit button on AHAH

Thanks for the clarification. I think it is the nearest submit button that gets activated when an enter button is pressed while a control is in focus. So probably it is the same behavior with this.form.submit.

Hiding the submit with css

Thanks for the article -- I was really banging my head against the FAPI's brick wall...

Regarding hiding the ahah submit button with CSS, the alternative is to render it using drupal_render in your theme function... then don't bother including that bit of render. It will still get called when you make the ahah call.

i.e,

function theme_mymodule_form($form){
  $trash = drupal_render($form['ahah_submit']);
  return drupal_render($form);
}

I really wish I didn't have

I really wish I didn't have to say it but maybe the problem isn't so much with Drupal but with its numerous Swiss cheese tutorials.

As clearly written and professionally laid out as this tutorial is, it suffers from the same flaw that makes me tear my hair out time and time again with other Drupal tutorial; the ???? step.

In this case, it's...

// ... [steps to retrieve, process and rebuild the form] ...

Couldn't you have copied and pasted the code from http://drupal.org/node/331941? Why make people jump all over the place looking for the parts to make the tutorial work?

Also, it would have been nice to include the hook_menu parts for those who weren't aware of them.

Even after following all of the steps listed in this tutorial and at http://drupal.org/node/331941, my module doesn't work and since the example here is incomplete, I can't really tell where I'm going wrong.

I appreciate the time you take to try to make the tutorial but an incomplete one is bound to waste someone else's time.

How could I set my form

How could I set my form function if I'm creating a node module using AHAH callbacks?

My function is declared as:
function departamento_form(&$node, $form_state)

Could I use this declaration instead?
function departamento_form(&$node, $form_state, $departamento = array())

thanks

thanks, I tried to write a module names "testahah"according to the tutorial it works fine.But I have a problem,when I tried to add AHAH dynamic form to an content type by using hook_form_alter,the form works find on Firefox but fail on IE & OPERA with error:warning: call_user_func_array() [function.call-user-func-array]: First argument is expected to be a valid callback, '' was given in /var/www/tuangou/includes/form.inc on line 366.

I am still looking for solution for that,anyway,I'v add a link of this article to my site www.webmasterclip.com in order to share it with more people.

Ok, just need to know one thing

My form is very simple. I got a text field and two selectboxes. The first is for the user to introduce a name. The second is for selecting an item which will change values on the second, just as your example does.

Doing things like you show (very nice post, by the way) I have two submit buttons which their submit functions. The first one is the CSS-hidden button on your example and the second is the final values processing submit (as for storing the values on the database).

My question is: ┬┐How do you force Drupal to only do the process of the hidden button submit when doing ahah callback? It stores the form on the database before setting the second selectbox to the values I want, just after selecting the first selectbox.

Thanks for the writeup. I

Thanks for the writeup. I started considering this approach for a node content form where I want some dependent nodereference select fields, but I decided against it for several reasons.

Here is what I want to do: I have three noderef fields in a form. One of them starts with a few options, the other two start empty and display options dependent on selections on the first. In other words, the AHAH would need to update *two* fields. I guess I could put a div around these two field elements and follow your instructions, but then I'd have to hope that down the road someone didn't reorder the two fields in the 'Manage Fields' page for that content type. That seems to make this AHAH approach risky for form_altering node forms with CCK fields.

Instead, I'm going to display the proper options for the second two fields using jQuery instead. And it avoids the problem of illegal values because I let noderef generate all the field values, and then remove them in a #pre_render function. So as long as I only add legal values with jQuery, it works fine. (At least in my preliminary tests using Firebug.)

I haven't been following the D7 AHAH modifications -- will this problem of affecting multiple CCK fields continue to be a problem with the D7 AHAH setup?

CCK AHAH difficulties

Hi Marco,
yes, I've noticed in the past that it's a lot trickier when it comes to cck fields, in particular noderef selects, and have also resorted to using jQuery to remove options, seeing as that doesn't cause FAPI to throw a fit. None of the D7 enhancements that I'm aware of will help this - there needs to be a CCK AHAH initiative, I'll try to find out if anyone's working on it.

Katherine

wow

Very well-written blog post on one of the most-often confused subjects in Drupal. Great job!