5

I have an AJAX button that should update form fields according to data already provided by user:

 'ajax_button' => array(
    '#type' => 'button',
    '#value' => t("Scan for tags"),
    '#ajax' => array(
      'callback' => 'ajax_tag',
      'wrapper' => 'dynamic-tags-fieldset',
      'method' => 'replace',
      'event' => 'click',
      'prevent' => 'submit click mousedown',
    ),
  ),

It works, but only if form is valid. Of course I do not want to validate it just yet. I don't care if all fields are filled, I only need my form to be updated at this very moment. How can I make form api not to invoke hook_form_validate() and not test #required fields?

9

You can use the #limit_validation_errors property:

Provides an array of sections which are parts of $form_state['values'] which should be validated, implying that sections which are not listed should not be validated. This is normally used in multistep forms in the case of a "back" button, for example, where '#limit_validation_errors' => array() would mean not to validate anything as form values on the current page are to be discarded anyway. #limit_validation_errors does not have any effect if #submit is not set. More discussion is in the form_set_error() documentation.

e.g.

'ajax_button' => array(
    '#type' => 'button',
    '#value' => t("Scan for tags"),
    '#ajax' => array(
      'callback' => 'ajax_tag',
      'wrapper' => 'dynamic-tags-fieldset',
      'method' => 'replace',
      'event' => 'click',
      'prevent' => 'submit click mousedown',
    ),
    '#limit_validation_errors' => array(), // No elements will be validated
    '#submit' => array('custom_function'), // An explicit submit functions array is required for this, but may be empty
  ),

However, each validation function is free to respect '#limit_validation_errors' or not. If you want your validation functions to respect this parameter, you need to:

  • Obtain #limit_validation_errors value from clicked button using $form_state['clicked_button']['#limit_validation_errors']
  • Pass it as a third argument to each form_set_error() call
  • Nope, not working. I added '#limit_validation_errors' => array(), '#submit' => array(), to my button but the validation is fired anyway. I cleared cache liberally with no result. – Mołot May 17 '13 at 11:40
  • As I understand it #submit can't be empty, it needs to point to the submission handler for that button – Clive May 17 '13 at 11:43
  • Nope, it can be empty all right. I'll edit your answer, as you provided only half of it and I just found second half. Edit: Done. And now it works. – Mołot May 17 '13 at 11:46
  • 1
    I've never come across that behaviour before, and according to the form_set_error() docs: "Partial form validation is implemented by suppressing errors rather than by skipping the input processing and validation steps entirely". So while what you're saying is true, the error messages will definitely be suppressed without needing to pass anything extra to form_set_error(). I use #limit_validation_errors a lot and I've never come across that issue. Indeed that argument is for "Internal use only", per the docs so shouldn't be set by a custom form – Clive May 17 '13 at 11:57
  • "$limit_validation_errors: Internal use only. The #limit_validation_errors property of the clicked button, if it exists." - well, what I added should NOT be needed, according to this, but it IS needed now in 7.22. Do you think I should report it as a core bug at drupal.org? – Mołot May 17 '13 at 11:59
0

I had similar issues, that I was unable to fix with #limit_validation_errors. So I found a dirty hack that I want to share, but strongly advise against, unless you know what you are doing.

Caveat / disclaimer:

  • I am still not fully aware myself of the consequences of this hack. I am still experimenting.
  • This answer is not a complete tutorial. It is meant for people who already know all the "official" information about form API.
  • There may be other, better ways to solve my specific problem. However, I still want to share this, because it provides a way to skip the entire validation + submission + rebuilding step. So even if in my case it turns out to be not necessary, for other it might be useful or interesting.
  • As said: Do not try this at home!

Use case / scenario

I was developing a custom form element type.

The element contains a button to add more items, which should change the form structure and let you continue with the modified form.

Observations about form API

Currently, if you have a form element with a '#process' callback and a '#value_callback' callback, and you click a submit button, each of these callbacks will be called twice during the POST request.

Why is this?

Let's have a look at drupal_process_form(). At first, this calls form_builder() directly, which recursively visits all form elements and calls #process, #value_callback and other callbacks.

Then, if $form_state['process_input'] is TRUE, it will call drupal_validate_form(), execute the submit handlers, and call drupal_rebuild_form(). The drupal_rebuild_form() calls form_builder(), which again fires all the element callbacks.

Between and during these two steps, there is logic that can mess with $form_state['values'] and $form_state['input']. So, if you have very dynamic form elements, it could happen that on the second call to #value_callback, you have a different result than on the first such call.

Overall, it would be nice if we could just skip the entire validation + submission altogether, if there was no actual submit button clicked.

Skip validation + submission + rebuilding?

In drupal_process_form(), we see this code:

  $form = form_builder($form_id, $form, $form_state);

  // Only process the input if we have a correct form submission.
  if ($form_state['process_input']) {
    drupal_validate_form($form_id, $form, $form_state);

We see that the entire validation, submission, and rebuilding is only done if $form_state['process_input'] is true.

My first idea was to directly set $form_state['process_input'] = FALSE; in my custom form element's #value_callback. However, I noticed this has undesirable consequences, because other form_builder() logic depends on this being TRUE.

The trick was to set $form_state['process_input'] = FALSE; as the last thing that would happen before the check in drupal_process_form().

The place to do this is in a callback registered into $form_state['complete form']['#after_build'].

Step by step

  1. In the custom element's #process callback, add the button:

    $element['addmore'] = [ '#type' => 'button', '#name' => $element['#name'] . '[addmore]', '#value' => t('Add item'), ];

It is important that the #name of the button is set explicitly, otherwise Drupal will set $button['#name'] = 'op'.

Also note that this button does not have a #submit function. This is ok, because all the logic will be in the #value_callback.

Having the name on the button means that a click will be noticed in the $input parameter of the #value_callback.

  1. In the '#value_callback' of the custom form element:

    if (isset($input['addmore'])) { // Button was clicked. $form_state['complete form']['#after_build'][] = '_MYMODULE_form_after_build_unset_process_input'; // Add an "empty" item. $values[] = NULL; }

Note: This is not the complete #value_callback! It is your job to figure out the rest.

  1. Write the callback function:

    function _MYMODULE_form_after_build_unset_process_input(array $form, array &$form_state) { $form_state['process_input'] = FALSE; return $form; }

Due to the way this function is registered, it really fires at the correct time.

  1. Ajaxify.

The previous instructions give you a non-ajax version.

How to add AJAX behavior depends a lot on the use case, so I am going to omit this part.

Note: With AJAX, we can actually use $form_state['input']['_triggering_element_name'] in the #value_callback, to determine which button was clicked. Note that at this time, $form_state['triggering_element'] is not yet filled.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.