Nov
5
Prevent "AHAH the Right Way" in Drupal from Breaking with Validation
November 5, 2010 - 11:46am
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:
- Load form
- Activate AHAH element (which calls AHAH callback, rebuilds form, then recaches form)
- Submit form with an error
- Validation reloads the page with a re-rendered copy of the form and errors
- Fix form errors, and submit form
- 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).
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!
Goes right in the form function. If it's not working, you may have a different problem or need to proof for typos.
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?
I haven't looked at D7's AHAH yet.
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.
No, that goes in the AHAH callback. Read the links to AHAH how-to articles I put at the beginning of the post.
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.
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. :)
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?
Can someone draw a pretty picture of what's going on? :-)
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
You sir are awesome.
Thank you for the fix...!
Thank you for the fix. It simply works.
Thanks.
This is what I was looking for. Indeed, you sir, are awesome. Thanks!
Thank you so much! I have spent more than 10 hours trying to figure out this. Thank you!
Ah! I saw this issue on at least 3 other tutorials but finally you put it in simple terms.
Post new comment