Validating required invisible FAPI fields in Drupal 7

Drupal 7 provides a form API for constructing both simple and dynamic forms. Dynamic forms display their fields based on user interactions with the form – such as selecting an option from a select list. There are really two ways to make a dynamic form:

  1. by using the #ajax property to rebuild the form, with new or modified fields, as a result of user interaction; or
  2. by using the #states property to hide or unhide fields as a result of user interaction.

Although the #ajax approach offers more flexibity, I prefer to use the #states property, where possible. The page refresh is faster and it, generally, requires less code. In the simple example, below, the 'Language' select list only appears when the 'Canada' option is selected from the 'Country' select list.


$form['country'] = array(
  '#type' => 'select',
  '#title' => t('Country'),
  '#options' => array(
    'canada' => t('Canada'), 
    'united_states' => t('United states'),
  ),
);

$form['language'] = array(
  '#type' => 'select',
  '#title' => t('Language'),
  '#options' => array(
    'english' => t('English'), 
    'french' => t('French'),
  ),
  '#states' => array(
    'visible' => array(
      'select[name="country"]' => array(
        'value' => t('canada'),
      ),
    ),
  ),
);

However, there is a problem. If I modify the 'Language' field in my example, making it a required field (line 4), the form's logic will fail.


$form['language'] = array(
  '#type' => 'select',
  '#title' => t('Language'),
  '#required' => TRUE,
  '#options' => array(
    'english' => t('English'), 
    'french' => t('French'),
  ),
  '#states' => array(
    'visible' => array(
      'select[name="country"]' => array(
        'value' => t('canada'),
      ),
    ),
  ),
);

Why? Because all form elements are validated when a form is submitted – even the invisible ones. Invisible fields will always be empty because the user cannot see them, and by default, if an empty required field is submitted it will fail validation. This puts the user in a situation where they get an error message, every time they try to submit the form, about a field that doesn't appear on the form.

There are several proposed solutions to solving this problem, but, as far as I know, only one that works fully. Let's start by looking at the others first.

Limit the validation errors

Before I outline this approach, I must concede that it doesn't appear to work; however, some propose this solution as the logical way to solve the problem. Two properties are key to this approach: #limit_validation_errors and #element_validate. The #limit_validation_errors property takes, as its single argument, an array of form elements that should be validated; #element_validate takes an array of validation functions to be called to validate a particular form element (and/or its children).


$form['language'] = array(
  '#type' => 'select',
  '#title' => t('Language'),
  '#required' => TRUE,
  '#options' => array(
    'english' => t('English'), 
    'french' => t('French'),
  ),
  '#states' => array(
    'visible' => array(
      'select[name="country"]' => array(
        'value' => t('canada'),
      ),
    ),
  ),
  '#element_validate' => array('my_module_language_validate'),
);


$form['submit'] => array(
  '#type' => 'submit',
  '#limit_validation_errors' => array(array('language')),
);


/**
 * Custom validate handler.
 */
function my_module_language_validate($element, &$form_state, $form) {
  if (empty($element['#value'])) {
    form_error($element, t('Please select a language.'));
  }
}

The logic of this approach follows that, on submission of the form, only the specified elements will be validated by the validation functions that they, themselves, specify. Although, in my testing of this approach, I found that the presence of '#required' => TRUE, will call the default validation regardless.

Unset the error messages

This approach seeks to directly remove particular error messages before they get set. The function to acheive this comes from this comment. On testing, I found that it worked on the first failed validation, but not on the second (I received empty error messages).


$form['submit'] => array(
  '#type' => 'submit',
  '#validate' => array('my_module_validate'),
);


/**
 * Custom validate handler.
 */
function my_module_validate() {
  my_module_unset_form_error('language');
}


/**
 * Unset form error.
 */
function my_module_unset_form_error($name) {
  $errors = &drupal_static('form_set_error', array());
  $removed_messages = array();
  if (isset($errors[$name])) {
    $removed_messages[] = $errors[$name];
    unset($errors[$name]);
  }
  $_SESSION['messages']['error'] = array_diff($_SESSION['messages']['error'], $removed_messages);
  if (empty($_SESSION['messages']['error'])) {
    unset ($_SESSION['messages']['error']);
  }
}

Use custom validation

The general consenus on how to solve this problem seems to be: do not require any fields and create your own custom validation function(s). These are alternate functions to the default form validation function. This is the only approach I have found to work without any problems.

My approach is to remove '#required' => TRUE, from the form constructor altogether. If you want to theme your fields to look like a required field, you can call theme_form_required_marker and append the returned markup to your field titles. For select lists you may also add '' => t('- Select -'), to your options, then your field will look as if you had required it in the usual way. The final step is to add the #validate property to the appropriate button/submit element. This property takes an array of validation function names. Each function is passed $form and $form_state as arguments. Such functions should employ form_set_error(), if the form values do not pass validation.

Then use some kind of logic to ensure your validation functions are called only when you know their corresponding elements are visible to the user.


$form_required_marker = array('#theme' => 'form_required_marker');
$required = ' ' . drupal_render($form_required_marker);


$form['language'] = array(
  '#type' => 'select',
  '#title' => t('Language') . $required,
  '#options' => array(
    '' => t('- Select -'),
    'english' => t('English'), 
    'french' => t('French'),
  ),
  '#states' => array(
    'visible' => array(
      'select[name="country"]' => array(
        'value' => t('canada'),
      ),
    ),
  ),
);


$form['submit'] => array(
  '#type' => 'submit',
  '#validate' => array('my_module_validate'),
);


/**
 * Custom validate handler.
 */
function my_module_validate($form, &$form_state) {
  if ($country = $form_state['values']['country']) {
    switch ($country) {
      case 'canada':
      if (empty($form_state['values']['language'])) {
        form_set_error('language', t('Please select a language.'));
      }
      break;
  }
}

Or…try to use #ajax

If, however, you decide, after reading this, you don't want to use the #states property and, instead, decide to go for the #ajax approach, you can try toggling the #required property between TRUE and FALSE depending on the contents of $form_state[input]. In my experience, this approach can be problematic.