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 recaches 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).

Tags:
  1. Floris (not verified) on December 20, 2010 - 5:56am

    Hi there,

    This seems to be the solution I was looking for. But it doesn't work yet.

    Where do I need to place the if (isset($form_state['action'])) etc. code? It does not work when I place it in my hook "dt_offer_entry_form".

    Do I need to define the form's action two?

    Thanks!

  2. Josh on December 20, 2010 - 8:10am

    Goes right in the form function. If it's not working, you may have a different problem or need to proof for typos.

  3. Anonymous (not verified) on January 5, 2011 - 6:45pm

    I love geeks with blogs.

    Finally, I understood what was going wrong in my module and how to prevent it. Thanks for tracking this down. Do you know, if D7 suffers from the same problem?

  4. Josh on January 6, 2011 - 10:09am

    I haven't looked at D7's AHAH yet.

  5. Nicolas (not verified) on January 11, 2011 - 8:59am

    HI!, thanks very much for this code,
    i have a problem when a I pasted the code into my hook_for_alter,
    warning: array_shift() [function.array-shift]: The argument should be an array
    ( in this line i got ($form_id = array_shift($args);)

    warning: call_user_func_array() [function.call-user-func-array]: First argument is expected to be a valid callback, '' was given in /public_html/home/includes/form.inc on line 376.

    this code can be pasted in hook_form_alter?, i have to change something?

    thanks in advance!,

    Nicolas.

  6. Josh on January 11, 2011 - 10:39am

    No, that goes in the AHAH callback. Read the links to AHAH how-to articles I put at the beginning of the post.

  7. Anonymous (not verified) on February 13, 2011 - 9:48am

    As mentioned in this article, we should write a submit handler for the element your #ahah behaviour is bound to. The handler must retain user-submitted information in $form_state, which will then be used in the rebuilding of the form. This handler will be called during the execution of your callback
    but if after triggering our AHAH callback, validation will fail due to some reason (e.g. required field was not filled)submit handlers will be not executed. So the question is - where should I place the logic, that retain user-submited information in $form_state variable. On first look the solution is not to write submit handler at all, place all logic, that retain user-submitted information in $form_state, right in ahah-callback. What you can say about this?
    Thanks.

  8. Josh on February 14, 2011 - 5:25pm

    If validation fails, the form will be repopulated with the submitted values, so I don't think it's an issue. If you're going to do something with your form submission, you're going to NEED a submit handler. :)

  9. tubik_cn (not verified) on February 21, 2011 - 6:00am

    so, as I understand, before triggering ahah handler all required fields on form should be filled, in other case we will receive validation error after server side handler complete their execution? Is there some approaches to solve this problem - correctly process ahah elements on form which contains required fields?

  10. Yonas (not verified) on March 4, 2011 - 1:50am

    Can someone draw a pretty picture of what's going on? :-)

  11. Kamal Palei (not verified) on April 16, 2011 - 1:19am

    I experienced this problem. Your blog was very much helpful to me.

    You keep writing blogs.., it saved my time and efforts to great extent.

    Thanks
    kamal

  12. Mark From Canada (not verified) on September 9, 2011 - 3:29pm

    You sir are awesome.

    Thank you for the fix...!

  13. Jose M (not verified) on November 22, 2011 - 11:21am

    Thank you for the fix. It simply works.
    Thanks.

  14. chechopeefe (not verified) on November 24, 2011 - 12:07am

    This is what I was looking for. Indeed, you sir, are awesome. Thanks!

  15. Edith (not verified) on February 16, 2012 - 1:03am

    Thank you so much! I have spent more than 10 hours trying to figure out this. Thank you!

  16. mattwad (not verified) on February 20, 2012 - 11:10pm

    Ah! I saw this issue on at least 3 other tutorials but finally you put it in simple terms.

  17. Post new comment

    The content of this field is kept private and will not be shown publicly.
    • Web page addresses and e-mail addresses turn into links automatically.
    • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <blockquote>
    • Lines and paragraphs break automatically.
    • You can enable syntax highlighting of source code with the following tags: <code>, <blockcode>, <geshi>, <bash>, <c>, <cpp>, <csharp>, <css>, <drupal5>, <drupal6>, <html>, <js>, <mysql>, <php>, <python>, <rails>, <ruby>, <sql>, <text>, <mssql>, <xml>. Beside the tag style "<foo>" it is also possible to use "[foo]". PHP source code can also be enclosed in <?php ... ?> or <% ... %>.

    More information about formatting options