AHAH in Drupal: may it one day live up to its acronym
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:
- We make sure our form is going to be cached so it can be retrieved by our AHAH callback
- 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.
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
}
?>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],
);
?>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'),
);
?><?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;
}
?>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),
));
}
?>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:
Binding the get_instruments button to the onchange event
Submitted by Anoop John (not verified) on Thu, 04/15/2010 - 11:52.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
Submitted by katherine on Thu, 04/15/2010 - 13:34.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
Submitted by Anoop John (not verified) on Mon, 04/19/2010 - 04:03.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
Submitted by reuben (not verified) on Mon, 01/18/2010 - 20:55.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
Submitted by Roland (not verified) on Tue, 10/27/2009 - 06:52.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
Submitted by Alan Domladovac Silva (not verified) on Mon, 08/10/2009 - 06:08.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
Submitted by Edward.H (not verified) on Fri, 07/31/2009 - 20:56.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
Submitted by Dani (not verified) on Mon, 06/29/2009 - 02:58.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
Submitted by Marco Carbone (not verified) on Thu, 04/30/2009 - 12:13.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
Submitted by katherine on Thu, 04/30/2009 - 12:27.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
Submitted by Irakli Nadareishvili (not verified) on Tue, 04/28/2009 - 16:27.Very well-written blog post on one of the most-often confused subjects in Drupal. Great job!
To bad you only writed up
Submitted by Fons (not verified) on Mon, 04/12/2010 - 23:09.To bad you only writed up half of your tutorial. some parts of code and structure may be obvious to you.
I for example am strugglin' with this for an hour or 6 now to get this to work.
to bad!
I have just added a dependent
Submitted by katherine on Thu, 04/15/2010 - 13:26.I have just added a dependent dropdown example to the ahah_example module in the examples package: http://drupal.org/project/examples