So there has been a lot of blogging and documentation in the past about the "right" way to do AHAH in Drupal. I think these people make excellent points, provide good documentation, and are pushing these types of dynamic interactions in Drupal in the right direction. I think a lot what's out there about this "right way" to do AHAH in Drupal misses some critical issues, though. Maybe someone else has blogged about it already, but I'll share one pitfall that I learned to avoid while doing AHAH.

Specifically, if you have a form using AHAH, there are a series of events that can lead to the form breaking completely:

  1. Load form
  2. Activate AHAH element (which calls AHAH callback, rebuilds form, then re-caches form)
  3. Submit form with an error
  4. Validation reloads the page with a re-rendered copy of the form and errors
  5. Fix form errors, and submit form
  6. BOOM!

What happened? The form submits and you probably get a load of JSON output, and your browser's URL is at the AHAH callback location. WHAT?

So, what happened is this: during the AHAH callback, the form is rebuilt. However, during the form rebuild, the #action element on the form is set to the default... which is the current request URI, which will be the path to the AHAH callback. This is bad, because now when you have a form validation error and the form is re-rendered in its entirety (as opposed to just using a section of re-rendered form as during the AHAH callback), the form now has its #action property set to the path of the AHAH callback.

That is bad. There have been mentions of how to fix it, but here is how I did it:

So there is this semi-utility-function-like block of code that AHAH the "right way" depends on existing in the AHAH callback, which looks something like this:

// From http://drupal.org/node/331941
$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_state['post'] = $form['#post'] = $_POST;
$form['#programmed'] = $form['#redirect'] = FALSE;
drupal_process_form($form_id, $form, $form_state);
$form = drupal_rebuild_form($form_id, $form_state, $args, $form_build_id);

That final call to drupal_rebuild_form() does a couple things: it rebuilds the form array, and it caches the result. This means that we can't just change the $form variable here after our call to drupal_rebuild_form() -- we need to make our form generator function smart! To do that, I made an important change to the above code:

// From http://drupal.org/node/331941
$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_state['post'] = $form['#post'] = $_POST;
$form['#programmed'] = $form['#redirect'] = FALSE;
// Stash original form action to avoid overwriting with drupal_rebuild_form().
$form_state['action'] = $form['#action'];
drupal_process_form($form_id, $form, $form_state);
$form = drupal_rebuild_form($form_id, $form_state, $args, $form_build_id);

Notice that I stashed the form's original #action property (as loaded from the previously-cached version of the form) in the $form_state variable. Then, when drupal_rebuild_form() goes to work, this little bit of code in my form function makes sure that my #action is correct through AHAH+Validation:

$form['#cache'] = TRUE; // Make sure the form is cached.

// Pull the correct action out of form_state if it's there to avoid AHAH+Validation action-rewrite.
if (isset($form_state['action'])) {
  $form['#action'] = $form_state['action'];
}

Now when the form is rebuilt during my AHAH call, it explicitly sets the #action property, which prevents the #action overwrite when drupal_prepare_form() (which is called by drupal_rebuild_form()) adds the result of _element_info('form') to the form (adding an array to another will add keys from the second array to the first, but not overwrite keys in the first that are also present in the second -- so $form['#action'] being present already prevents it from being overwritten).