Using the Drupal 7 Form API states system to create conditions between form elements

Monday, January 20, 2014 - 09:43

In this article we will look at how you can leverage the power of the Drupal 7 Form API states system to create dependencies between your form fields. In other words, when you programmatically write your forms, you’ll be able to make the behaviour of the elements depend on others.

To illustrate this power, we’ll write some code, but first, let’s see some theory.

What are the states and what do they do?

The states are simple properties or HTML attributes of DOM elements. When creating your form elements using the Form API, you can apply various states to them depending on the states of others. Thus there are 2 kinds of elements: those which act as the condition for others and those which depend on these conditions to change their state. I like the way Jeff Robbins of Lullabot categorises the elements by the possible states they can have: callers and listeners.

The possible states for the first group (the ones that trigger change in others)

  • empty
  • filled
  • checked
  • unchecked
  • expanded
  • collapsed
  • value

The possible states for the second group (the ones that get applied onto elements):

  • enabled
  • disabled
  • required
  • optional
  • visible
  • invisible
  • checked
  • unchecked
  • expanded
  • collapsed

And what does Drupal do? It provides all the necessary javascript to make all these behavioral alterations on the fly so you don’t have to worry about javascript or jQuery.

Example module

To illustrate how this all works, we are going to build a tiny module which declares a path to a form. Using this form, we will play around with some elements and see all this in action. So let’s dive in. You can download this small module if you want to follow along.

Assuming you have done the necessary to create your empty module, we can go ahead and declare the menu path using hook_menu():

/**
 * Implements hook_menu().
 */
 
function states_menu() {
 
  $items['states-example'] = array(
    'title' => 'Demonstrating the Form API states system',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('states_example_form'),
    'type' => MENU_NORMAL_ITEM,
    'access arguments' => array('access content'),
  );
 
  return $items;
}

This registers the states-example path which will render the states_example_form declared below:

function states_example_form($form, &$form_state) {
 
  $form = array();
 
  $form['name'] = array(
    '#title' => t('Your name'),
    '#type' => 'textfield',
  ); 
 
  $form['submit'] = array(
    '#type' => 'submit', 
    '#value' => t('Submit'),
  );
 
  return $form;
}

If you clear the cache and go to that path, you should see the form with 2 form elements: the name field and the submit button. No biggie. One thing to keep in mind for later is that the array key 'name' will be the actual DOM name of the form element.

Lets now add another element below the name field that will depend on the latter and show up only if the user enters something in the name field:

  $form['preferences'] = array(
    '#title' => t('Your preference'),
    '#type' => 'select',
    '#options' => array('moto' => 'Motorcycles', 'cars' => 'Cars'),
     // Show this field only if the name field is filled.
    '#states' => array(
      'visible' => array(
        'input[name="name"]' => array('filled' => TRUE),
      ),
    ),
  );

Lets scan through this array and see what’s what. Above the comment we can see that the element is a select list with 2 options and a title. Below the comment we declare that to this element will be applied the state visible when and if the DOM element of the type input with the name="name" (your classic css/jQuery selector and which happens to be the first element in our form) gets filled. Otherwise, the opposite state is applied - in this case, invisible.

So in the browser when the form is loaded on the page, only the name field is visible. When the user starts typing the name, the preferences field appears. Refresh the page and check it out. Drupal adds the necessary javascript for this to happen.

You can also do it the other way around. You can apply the state invisible when another element => array('filled' => FALSE). It will have the same effect except you declare it differently.

Next, let’s add another element that will depend on the preferences field:

  $form['brands'] = array(
    '#title' => t('Your favorite brand'),
    '#type' => 'select',
    '#options' => array('Toyota', 'BMW', 'Audi'),
     // The reverse of the previous field: hide this field if there is no name
    '#states' => array(
      'visible' => array(
        'select[name="preferences"]' => array('value' => 'cars'),
      ),
    ),
  );

This is another select list but the visibility of this element depends on the actual value of the previous one. So in the browser, this will only appear if and when the user selects that they prefer cars over motorcycles.

Let’s look at the next 2 elements:

  $form['marriage'] = array(
    '#title' => t('Are you married?'),
    '#type' => 'checkbox',
  );
 
  $form['spouse_preferences'] = array(
    '#title' => t('Your spouse\'s preference'),
    '#type' => 'radios',
    '#options' => array('moto' => 'Motorcycles', 'cars' => 'Cars'),
     // Show this field only if the user name is filled and if s/he is married.
    '#states' => array(
      'visible' => array(
        'input[name="name"]' => array('filled' => TRUE),
        'input[name="marriage"]' => array('checked' => TRUE),
      ),
    ),
  );

The first one is a simple checkbox that will show no matter what. But the second one is another select list and here we can see that the visibility of this depends on 2 previous elements. So this will show only when and if the user fills in a name and checks the box confirming that s/he is married. A so called AND condition, if you will. I will tell you write off the bat that you cannot do OR conditions with the states system - at least not that I could figure out. If you manage to find a way, let me know.

And the last two elements we will look at in this article:

  $form['kids'] = array(
    '#title' => t('How many kids do you have?'),
    '#type' => 'select',
    '#options' => array(0, 1, 2),
  );
 
  $form['kids_preferences'] = array(
    '#title' => t('Your kids\' preference'),
    '#type' => 'select',
    '#options' => array('moto' => 'Motorcycles', 'cars' => 'Cars'),
     // Show this field only if the user has kids.
    '#states' => array(
      'visible' => array(
        'input[name="marriage"]' => array('checked' => TRUE),
      ),
      'disabled' => array(
        'select[name="kids"]' => array('value' => '0'),
      ),
    ),
  );

Again the first one will show no matter what, but this time it's a select list. The second one depends on two other elements for two different state changes. First, it will become visible only if the the user checks the box that s/he is married. Second, it will become disabled if the user selects that s/he has 0 kids. Neat.

Conclusion

As i mentioned in the beginning of the article, there are a number of different states available for elements and Drupal handles all the javascript for you. We cannot go over all of them here nor try all the different combinations of what you can do with the states system. I thus encourage you experiment further on your own. You can even download this little module and use it as a starting point.

And as always, Drupal.org is your friend and you can find some more information on the states system at the following pages:

Comments

Thanks for this great writeup of Drupal form api's state system.

The "input[name="NAME"]"-selectors only worked when prepended by a colon, though: ":input[name="NAME"]".