Doing AHAH Correctly in Drupal 6
For an understanding about what AHAH is, and more specifically, what AHAH in Drupal is, please read the "Introduction to AHAH in Drupal" before reading this section. Here we will launch straight into the problem of adding dynamic form elements in Drupal without violating Form API security.
1. Understanding how your AHAH form should work
Once you have attached an #ahah binding to a form element, there is a pretty strict set-up involved in getting it to work according to best practices. Here is an overview of what needs to be in place:
- Your main function for building the form should react to $form_state, meaning that its elements and their values will be defined based on user-submitted data or data already existing in the database
- You need an AHAH callback function which is called by your #ahah binding
- Your AHAH callback retrieves the form from the cache and goes through the process of rebuilding it, which includes calling submit handlers, the first of which will be the element-specific submit handler for the element your #ahah behaviour is bound to
- Your submit handler needs to retain user-submitted information in $form_state which will then be used in the rebuilding of the form
Many people, when they first start trying to build an AHAH-enabled form, make the mistake of using the AHAH callback to make changes to the form, e.g. adding in a new element or altering an existing one. This is NOT what the AHAH callback is for. The callback simply retrieves, processes and rebuilds the form, then returns whatever portion of it needs to be re-rendered, replacing the originally rendered version of that portion.
So, how does your form actually get changed?
"Changes" to your form essentially translate as "different ways that your form can be built, depending on what's in $form_state". If you can grasp that, you are 90% of the way to nailing AHAH in Drupal.
2. The code you need to achieve this
There are essentially two main tricks beyond simply creating your form that you must follow for AHAH to work smoothly and soundly (and following these can also reduce security holes).
First of all - the form you see on screen and the $form array stored in the cache should always be the same.
Second, the form building function should create subsequent steps based on the contents of $form_state (which is populated in the form's submit handler), and recreate the whole form each time based only on $form_state and data already existing in the database. This function should not use $_POST or similar.
As an example, here is a small excerpt from the top of the function node_form in node.pages.inc:
<?php
if (isset($form_state['node'])) {
$node = $form_state['node'] + (array)$node;
}
?>While that may seem complex and hard to understand, it's really quite simple: we pretend that the user submitted data is already saved in the database and simply show the form that you will get the next time you edit the node.
If you follow the two tricks above, a nice cycle emerges:
- The form generator creates the form.
- It gets cached, and rendered.
- The user submits the AHAH callback, which goes to your callback.
- You retrieve the form from the cache.
- You process it with
drupal_process_form. Process calls the submit handlers, which put whatever was worthy of keeping into form_state. - You call
drupal_rebuild_formwhich first destroys$_POST. - The form generator function is called and creates the form again but since it knows to react to form_state, the form will be different.
- The new form gets cached and processed again, but because
$_POSTis destroyed, it knows not to call the submit handlers again. - Your AHAH callback picks a piece of the form and renders it.
This cycle leads to good and secure code, and also AHAH Zen.
Here's an example from poll.module* of AHAH done right.
<?php
function poll_choice_js() {
// The form is generated in an include file which we need to include manually.
include_once 'modules/node/node.pages.inc';
// We're starting in step #3, preparing for #4.
$form_state = array('storage' => NULL, 'submitted' => FALSE);
$form_build_id = $_POST['form_build_id'];
// Step #4.
$form = form_get_cache($form_build_id, $form_state);
// Preparing for #5.
$args = $form['#parameters'];
$form_id = array_shift($args);
$form_state['post'] = $form['#post'] = $_POST;
$form['#programmed'] = $form['#redirect'] = FALSE;
// Step #5.
drupal_process_form($form_id, $form, $form_state);
// Step #6 and #7 and #8.
$form = drupal_rebuild_form($form_id, $form_state, $args, $form_build_id);
// Step #9.
$choice_form = $form['choice_wrapper']['choice'];
unset($choice_form['#prefix'], $choice_form['#suffix']);
$output = theme('status_messages') . drupal_render($choice_form);
// Final rendering callback.
drupal_json(array('status' => TRUE, 'data' => $output));
}
?>Up until and including drupal_rebuild_form your code should be the very same. Yes, it should be a utility function, and it is in D7.
The following example, which comes from quicktabs.module, shows how the above works for non-node forms. There are three #ahah elements in the quicktabs admin form (this form allows you to create blocks of tabbed content, choosing either an existing block or a view for each tab): a button allowing you to add an extra tab to the form (essentially an extra set of elements); a button allowing you to remove a tab from the form; and a views dropdown which, when changed, instantly updates the view display options in another dropdown.
Below is the submit handler for the "add tab" button, followed by the ahah callback for all three ahah elements:
<?php
/**
* submit handler for the "Add Tab" button
*/
function qt_more_tabs_submit($form, &$form_state) {
unset($form_state['submit_handlers']);
form_execute_handlers('submit', $form, $form_state);
$quicktabs = $form_state['values'];
$form_state['quicktabs'] = $quicktabs;
$form_state['rebuild'] = TRUE;
if ($form_state['values']['tabs_more']) {
$form_state['qt_count'] = count($form_state['values']['tabs']) + 1;
}
return $quicktabs;
}
/**
* ahah callback
*/
function quicktabs_ahah() {
$form_state = array('storage' => NULL, 'submitted' => FALSE);
$form_build_id = $_POST['form_build_id'];
$form = form_get_cache($form_build_id, $form_state);
$args = $form['#parameters'];
$form_id = array_shift($args);
$form['#post'] = $_POST;
$form['#redirect'] = FALSE;
$form['#programmed'] = FALSE;
$form_state['post'] = $_POST;
drupal_process_form($form_id, $form, $form_state);
$form = drupal_rebuild_form($form_id, $form_state, $args, $form_build_id);
$qt_form = $form['qt_wrapper']['tabs'];
unset($qt_form['#prefix'], $qt_form['#suffix']); // Prevent duplicate wrappers.
$javascript = drupal_add_js(NULL, NULL, 'header');
drupal_json(array(
'status' => TRUE,
'data' => theme('status_messages') . drupal_render($qt_form),
'settings' => call_user_func_array('array_merge_recursive', $javascript['setting']),
));
}
?>Here we don't need to include modules/node/node.pages.inc because the function that generated the form is in quicktabs.module itself. Notice that the change is being made to the form in the submit handler (incrementing the number of tabs by 1), which is called from
drupal_process_form. For this to work, the function generating the form needs to react to $form_state:<?php
// if the form is being generated from an ahah callback, $form_state['quicktabs'] will
// contain the posted values of the form - if it's an edit form, the contents of
// $quicktabs will be coming from the database
if (isset($form_state['quicktabs'])) {
$quicktabs = $form_state['quicktabs'] + (array)$quicktabs;
}
// how many sets of tab elements do we want to generate - check $form_state first
if (isset($form_state['qt_count'])) {
$qt_count = $form_state['qt_count'];
}
else {
$qt_count = max(2, empty($tabcontent) ? 2 : count($tabcontent));
}
?>Again you can see how $quicktabs = $form_state['quicktabs'] + (array)$quicktabs; we pretend that the user submitted data is already saved.
Finally, here is the submit handler for the views dropdown:
<?php
/**
* submit handler for the Views drop down
*/
function qt_get_displays_submit($form, &$form_state) {
unset($form_state['submit_handlers']);
form_execute_handlers('submit', $form, $form_state);
$quicktabs = $form_state['values'];
$form_state['quicktabs'] = $quicktabs;
$form_state['rebuild'] = TRUE;
return $quicktabs;
}
?>Notice that it doesn't seem to make any change to the form at all - the form is simply going to be rebuilt but with a different selected value for this dropdown, and that will change the options in the display dropdown. The ahah callback for this is the one shown above, which is also the callback used for the remove button, it just has a different submit handler, which decreases the number of tabs by one.
One other thing worth mentioning about the ahah callback is the use of the $javascript array. This trick comes from Wim Leers' AHAH Helper module and enables the re-attachment of ahah behaviors to the ahah-generated form items. Normal jQuery behaviors get re-attached anyway if they are used within Drupal.behaviors, but ahah behaviors only get attached to elements specified in Drupal.settings.ahah. This trick is not necessary for poll.module because the form elements themselves (textfields for entering poll choices) don't have any ahah functionality, but in the case of Quick Tabs, a new Views dropdown is being included with each new set of elements that's added via the "Add tab" button and this needs an ahah behavior attached to it.
To see the full code, visit http://drupal.org/project/quicktabs and download the quicktabs-6.x-2.0-rc1 release. To see the form in action, click here. You will need to change the tab type to "View" in order to see the views display dropdown working.
* the code shown from poll.module is from the D7 version, the ahah callback has not yet been changed in D6
Caution With File Uploads
Using something similar to the above with the 'file' type didn't work. It does work well to add fields, but you have a file to upload, the browser (any old browser) throws up the notorious "HTTP error 0" message.
The problem is detailed much more explicitly here:
http://drupal.org/node/399676
In a nutshell, the code here works perfectly, unless the field type is 'file'

Cannot submit more than once
I have the example above copied almost line for line except step 9.
When I do the ahah submit the first time, I get the following message without the return value I specified:
Page test has been updated.Then, every submit after that I get the return value and the message:
This content has been modified by another user, changes cannot be saved.Any idea what I could be doing wrong?
Also, the ahah bits are part of a custom CCK widget. Don't know if that matters.
Resolved
Okay so I figured it out. In step #3, setting 'submitted' => FALSE does not work. Instead, I set 'rebuild' => TRUE.
$form_state = array('storage' => NULL, 'rebuild' => TRUE);This causes my forms to rebuild only, not submit.
When I looked at the drupal_process_form, it checks to see if 'submitted' is not empty. By setting 'submitted' => FALSE, it is no longer empty so the function thinks it is a submit. This is probably a bug.
Modifying a CCK node add form with AHAH
Say you've defined your own content type and you'd like to manipulate an element on the node add form using AHAH. In my case, I want to change the value in a textfield based on the value of another element.
The process is:
1. Create a CCK field whose value you want to change.
2. Force drupal to cache your form (in hook_form_alter)
3. Create your menu item (in hook_menu)
4. Link to the menu url in your ahah property of the element you want to trigger the change
5. Write a function to change the value of the element, and make sure that gets stored in $form
Say that we want to change the value of field_flowers to "roses" when a button is clicked (yes, it's a pretty arbitrary example, but the logic will apply for changing the value of any other form element based on an AHAH event). Fleshing out each of the above steps:
1. Create a CCK field whose value you want to change.
In admin/content/types choose the CCK content type you want to work with, and add a text field - call it field_flower. We'll be assigning it the value of 'roses' later.
2. Force drupal to cache your form (in hook_form_alter) by putting a
$form['#cache'] = TRUE;in the relevant spot, like so:function mymodule_form_alter(&$form, $form_state, $form_id) {switch ($form_id) {
case 'my_cck_type_node_form':
$form['#cache'] = TRUE;
break;
}
}
We force drupal to cache the form because it doesn't for a node add form. Once we've clicked on the element which triggers the change, we need to access $form and change some things within it - and we can only access $form if it's cached.
3. Create your menu item (in hook_menu)
$items['ahah-change-flower'] = array('page callback' => 'mymodule_change_flower',
'access callback' => TRUE,
'type' => MENU_CALLBACK,
);
4. Link to the menu url in your ahah property of the element you want to trigger the change
In your hook_form_alter() function, add the following (after the $form['#cache'] = TRUE; line):
$form['field_my_button'] = array('#type' => 'button',
'#value' => t('Insert new flower'),
'#ahah' => array(
'event' => 'click',
'path' => 'ahah-change-flower',
'wrapper' => 'edit-field-flower-nid-nid-wrapper',
'method' => 'replace',
),
);
In this case we've added AHAH to a new button (called 'field_my_button') and asked it to load up the url 'ahah-change-flower' (which is loaded in the background because that's the nature of AHAH - it's not a full page reload). Once that url is requested drupal calls the function mymodule_change_flower() - which is our next step.
Note that the value of 'wrapper' is the id of the div which field_flower sits in. To find the value of that particular id, view the source code of your form and look for field_flower. Firebug is your friend!
5. Write a function to change the value of the element, and make sure that gets stored in $form
Somewhere in your module, add this:
function mymodule_change_flower() {
// The form is generated in an include file which we need to include manually.
include_once 'modules/node/node.pages.inc';
// We're starting in step #3, preparing for #4.
$form_state = array('storage' => NULL, 'submitted' => FALSE);
$form_build_id = $_POST['form_build_id'];
// Step #4.
$form = form_get_cache($form_build_id, $form_state);
// Preparing for #5.
$args = $form['#parameters'];
$form_id = array_shift($args);
$form_id = $args;
$form_state['post'] = $form['#post'] = $_POST;
$form['#programmed'] = $form['#redirect'] = FALSE;
// Step #5.
//drupal_process_form($form_id, $form, $form_state);
// Step #6 and #7 and #8.
// $form = drupal_rebuild_form($form_id, $form_state, $args, $form_build_id);
//change your element here
$form['field_flower']['#value'] = 'roses';
form_set_cache($form_build_id, $form, $form_state);
$form += array(
'#post' => $_POST,
'#programmed' => FALSE,
);
$form = form_builder('node_form', $form, $form_state);
// Step #9.
$subform = $form['field_flower'];
unset($choice_form['#prefix'], $choice_form['#suffix']);
$output = theme('status_messages') . drupal_render($subform);
// Final rendering callback.
drupal_json(array('status' => TRUE, 'data' => $output));
}
Note how similar this code is to the 9 steps above, yet also different. Firstly, since we're adding or editing a content type, we need to include the relevant form generating function, which is in node.pages.inc (the function is called node_form() if you're interested).
Note that step 5 and step 6/7/8 are commented out - we don't need them. I found that executing these is unnecessary and actually removes the values I want to access in $form. Simply retrieving $form from the cache is enough to make the changes; then save $form to the cache again, rebuild the form, and render it.
This code was taken from Wim Leers post on his AHAH helper module, about why AHAH in drupal is so hard.
6. Summary
So what we've done is set field_flower to the value of "roses" when a button is clicked. Equally, we could change the value of field_flower if we were change the value of a select list, by attaching the #ahah property to the relevant select list.
This logic will work for most CCK field types, but it doesn't work for what I originally researched this for: changing the values in a node reference select list when another element's values change. I hope to find a solution for that soon!
Hi burningdog - unfortunately
Hi burningdog - unfortunately the method you're using here is actually the "old" way, and it goes against the best practices described on this page. I'm pretty sure Wim Leers's module no longer uses this method.
I have added a new introduction to the top of this page which I hope will clarify things somewhat.
Dealing with AHAH and cck select field
Hi Roger,
I'm just now dealing with AHAH and CCK select list, but all the documentation I found it's not enought to solve that issue.
Did you find something in this way?
Thanks in advance
AHAH Submit
.