Form API #states

Dynamic forms without the custom JavaScript

Drupal's Form API helps developers build complex, extendable user input forms with minimal code. One of its most powerful features, though, isn't very well known: the #states system. Form API #states allow us to create form elements that change state (show, hide, enable, disable, etc.) depending on certain conditions—for example, disabling one field based on the contents of another. For most common Form UI tasks, the #states system eliminates the need to write custom JavaScript. It eliminates inconsistencies by translating simple form element properties into standardized JavaScript code.

The syntax of the #state property is:

  
#states' => array(
  'STATE' => array(
    JQUERY_SELECTOR => REMOTE_CONDITIONS,
    JQUERY_SELECTOR => REMOTE_CONDITIONS,
    ...
  ),
),
  

Each form field consist of states and remote conditions that define the properties of the field. A state is a property that can be applied to a form element (i.e. enabled, disabled, checked, unchecked), while a remote condition is the state of an element to trigger a change in different element. All the available states and remote conditions are defined in the drupal_process_states(). Wunderkraut also has a great article, quoting our Co-Founder and CEO Jeff Robbins breaking down the states into two categories, "the ones that trigger change in others" and "the ones that get applied onto elements".

Starting with a simple form, let's look at a few examples.

Here is a simple form to collect some basic personal info from a user.

Simple form

We would like the name field and anonymous checkbox to work together. We can simply use the invisible state to hide the name field when the anonymous checkbox is checked, and conversely keep the anonymous checkbox unchecked if the name field is filled.

  
// Hide name field when the anonymous checkbox is checked.
$form['name'] = array(
  '#type' => 'textfield',
  '#title' => t('Name'),
  '#states' => array(
    'invisible' => array(
      ':input[name="anonymous"]' => array('checked' => TRUE),
    ),
  ),
);

// Uncheck anonymous field when the name field is filled.
$form['anonymous'] = array(
  '#type' => 'checkbox',
  '#title' => t('I prefer to remain anonymous'),
  '#states' => array(
    'unchecked' => array(
      ':input[name="name"]' => array('filled' => TRUE),
    ),
  ),
);
  

We can also define multiple conditions to show the email field, only when the name field is filled and email is the preferred method of contact.

  
// Show the email field when the name field is filled
// and 'email' is selected for the preferred method of contact field
$form['email'] = array(
  '#type' => 'textfield',
  '#title' => t('Email'),
  '#states' => array(
    'visible' => array(
      ':input[name="name"]' => array('filled' => TRUE),
      ':select[name="method"]' => array('value' => 'email'),
    ),
  ),
);
  

For OR conditions, use an non-associative array by wrapping each group of conditions in the array() function.

  
// Show the email field when either 
// * the name is filled and the method is email, 
// * or anonymous is checked and method is email.
$form['email'] = array(
  '#type' => 'textfield',
  '#title' => t('Email'),
  '#states' => array(
    'visible' => array(
      array(
        ':input[name="name"]' => array('filled' => TRUE),
        ':select[name="method"]' => array('value' => 'email'),
      ),
      array(
        ':input[name="anonymous"]' => array('checked' => TRUE),
        ':select[name="method"]' => array('value' => 'email'),
      ),
    ),
  ),
);
  

There is also a special syntax for the exclusive or (XOR) operator, which one or the other condition is true, but not both. While the OR operator is implied by using an non-associative array, the XOR operator needs to be explicitly defined with an array item with the string 'xor'. Let's change the email field to accommodate users that prefer to be anonymous.

  
// Show the email field when either condition is true, but not both
// * the name is filled and the method is email, 
// * anonymous is checked and method is email.
$form['email'] = array(
  '#type' => 'textfield',
  '#title' => t('Email'),
  '#states' => array(
    'visible' => array(
      array(
        ':input[name="name"]' => array('filled' => TRUE),
        ':select[name="method"]' => array('value' => 'email'),
      ),
      'xor',
      array(
        ':input[name="anonymous"]' => array('checked' => TRUE),
        ':select[name="method"]' => array('value' => 'email'),
      ),
    ),
  ),
);
  

Conclusion

Form API #states is a great way to generate consistent JavaScript code for simple form interactions. Although custom JavaScript might be necessary for more complex requirements, the #states system gives us a very good start in creating centralized and standardized frontend code. I find the #state system generally much cleaner in terms of code maintenance. Compared to custom JavaScript, it's also less prone to bugs and accessibility issues.

Further reading

Published in:

Get in touch with us

Tell us about your project or drop us a line. We'd love to hear from you!