7.x-1.x form_example_states.inc form_example_states_form($form, &$form_state)

States demo form.

This form shows off the #states system by dynamically showing parts of the form based on the state of other parts.

The basic idea is that you add a #states property to the element which is to be changed based on some action elsewhere on the form. The #states property lists a change which is to be made, and under what conditions that change should be made.

For example, in the 'tests_taken' form element below we have:

'#states' => array(
  'visible' => array(
    ':input[name="student_type"]' => array('value' => 'high_school'),
  ),
),

Meaning that the element is to be made visible when the condition is met. The condition is a combination of a jQuery selector (which selects the element we want to test) and a condition for that element. In this case, the condition is whether the return value of the 'student_type' element is 'high_school'. If it is, this element will be visible.

So the syntax is:

'#states' => array(
  'action_to_take_on_this_form_element' => array(
    'jquery_selector_for_another_element' => array(
      'condition_type' => value,
    ),
  ),
),

If you need an action to take place only when two different conditions are true, then you add both of those conditions to the action. See the 'country_writein' element below for an example.

Note that the easiest way to select a textfield, checkbox, or select is with the ':input' jquery shortcut, which selects any any of those.

There are examples below of changing or hiding an element when a checkbox is checked, when a textarea is filled, when a select has a given value.

See drupal_process_states() for full documentation.

See also

forms_api_reference.html

Related topics

1 string reference to 'form_example_states_form'
form_example_menu in form_example/form_example.module
Implements hook_menu().

File

form_example/form_example_states.inc, line 63
An example of how to use the new #states Form API element, allowing dynamic form behavior with very simple setup.

Code

function form_example_states_form($form, &$form_state) {
  $form['student_type'] = array(
    '#type' => 'radios',
    '#options' => array(
      'high_school' => t('High School'),
      'undergraduate' => t('Undergraduate'),
      'graduate' => t('Graduate'),
    ),
    '#title' => t('What type of student are you?'),
  );
  $form['high_school'] = array(
    '#type' => 'fieldset',
    '#title' => t('High School Information'),
    // This #states rule says that the "high school" fieldset should only
    // be shown if the "student_type" form element is set to "High School".
    '#states' => array(
      'visible' => array(
        ':input[name="student_type"]' => array('value' => 'high_school'),
      ),
    ),
  );

  // High school information.
  $form['high_school']['tests_taken'] = array(
    '#type' => 'checkboxes',
    '#options' => drupal_map_assoc(array(t('SAT'), t('ACT'))),
    '#title' => t('What standardized tests did you take?'),
    // This #states rule says that this checkboxes array will be visible only
    // when $form['student_type'] is set to t('High School').
    // It uses the jQuery selector :input[name=student_type] to choose the
    // element which triggers the behavior, and then defines the "High School"
    // value as the one that triggers visibility.
    '#states' => array(
      // Action to take.
      'visible' => array(
        ':input[name="student_type"]' => array('value' => 'high_school'),
      ),
    ),
  );

  $form['high_school']['sat_score'] = array(
    '#type' => 'textfield',
    '#title' => t('Your SAT score:'),
    '#size' => 4,

    // This #states rule limits visibility to when the $form['tests_taken']
    // 'SAT' checkbox is checked."
    '#states' => array(
      // Action to take.
      'visible' => array(
        ':input[name="tests_taken[SAT]"]' => array('checked' => TRUE),
      ),
    ),
  );
  $form['high_school']['act_score'] = array(
    '#type' => 'textfield',
    '#title' => t('Your ACT score:'),
    '#size' => 4,

    // Set this element visible if the ACT checkbox above is checked.
    '#states' => array(
      // Action to take.
      'visible' => array(
        ':input[name="tests_taken[ACT]"]' => array('checked' => TRUE),
      ),
    ),
  );

  // Undergrad information.
  $form['undergraduate'] = array(
    '#type' => 'fieldset',
    '#title' => t('Undergraduate Information'),
    // This #states rule says that the "undergraduate" fieldset should only
    // be shown if the "student_type" form element is set to "Undergraduate".
    '#states' => array(
      'visible' => array(
        ':input[name="student_type"]' => array('value' => 'undergraduate'),
      ),
    ),
  );

  $form['undergraduate']['how_many_years'] = array(
    '#type' => 'select',
    '#title' => t('How many years have you completed?'),
    // The options here are integers, but since all the action here happens
    // using the DOM on the client, we will have to use strings to work with
    // them.
    '#options' => array(
      1 => t('One'),
      2 => t('Two'),
      3 => t('Three'),
      4 => t('Four'),
      5 => t('Lots'),
    ),
  );

  $form['undergraduate']['comment'] = array(
    '#type' => 'item',
    '#description' => t("Wow, that's a long time."),
    '#states' => array(
      'visible' => array(
        // Note that '5' must be used here instead of the integer 5.
        // The information is coming from the DOM as a string.
        ':input[name="how_many_years"]' => array('value' => '5'),
      ),
    ),
  );
  $form['undergraduate']['school_name'] = array(
    '#type' => 'textfield',
    '#title' => t('Your college or university:'),
  );
  $form['undergraduate']['school_country'] = array(
    '#type' => 'select',
    '#options' => drupal_map_assoc(array(t('UK'), t('Other'))),
    '#title' => t('In what country is your college or university located?'),
  );
  $form['undergraduate']['country_writein'] = array(
    '#type' => 'textfield',
    '#size' => 20,
    '#title' => t('Please enter the name of the country where your college or university is located.'),

    // Only show this field if school_country is set to 'Other'.
    '#states' => array(
      // Action to take: Make visible.
      'visible' => array(
        ':input[name="school_country"]' => array('value' => t('Other')),
      ),
    ),
  );

  $form['undergraduate']['thanks'] = array(
    '#type' => 'item',
    '#description' => t('Thanks for providing both your school and your country.'),
    '#states' => array(
      // Here visibility requires that two separate conditions be true.
      'visible' => array(
        ':input[name="school_country"]' => array('value' => t('Other')),
        ':input[name="country_writein"]' => array('filled' => TRUE),
      ),
    ),
  );
  $form['undergraduate']['go_away'] = array(
    '#type' => 'submit',
    '#value' => t('Done with form'),
    '#states' => array(
      // Here visibility requires that two separate conditions be true.
      'visible' => array(
        ':input[name="school_country"]' => array('value' => t('Other')),
        ':input[name="country_writein"]' => array('filled' => TRUE),
      ),
    ),
  );

  // Graduate student information.
  $form['graduate'] = array(
    '#type' => 'fieldset',
    '#title' => t('Graduate School Information'),
    // This #states rule says that the "graduate" fieldset should only
    // be shown if the "student_type" form element is set to "Graduate".
    '#states' => array(
      'visible' => array(
        ':input[name="student_type"]' => array('value' => 'graduate'),
      ),
    ),
  );
  $form['graduate']['more_info'] = array(
    '#type' => 'textarea',
    '#title' => t('Please describe your graduate studies'),
  );

  $form['graduate']['info_provide'] = array(
    '#type' => 'checkbox',
    '#title' => t('Check here if you have provided information above'),
    '#disabled' => TRUE,
    '#states' => array(
      // Mark this checkbox checked if the "more_info" textarea has something
      // in it, if it's 'filled'.
      'checked' => array(
        ':input[name="more_info"]' => array('filled' => TRUE),
      ),
    ),
  );

  $form['average'] = array(
    '#type' => 'textfield',
    '#title' => t('Enter your average'),
    // To trigger a state when the same controlling element can have more than
    // one possible value, put all values in a higher-level array.
    '#states' => array(
      'visible' => array(
        ':input[name="student_type"]' => array(
          array('value' => 'high_school'),
          array('value' => 'undergraduate'),
        ),
      ),
    ),
  );

  $form['expand_more_info'] = array(
    '#type' => 'checkbox',
    '#title' => t('Check here if you want to add more information.'),
  );
  $form['more_info'] = array(
    '#type' => 'fieldset',
    '#title' => t('Additional Information'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,

    // Expand the expand_more_info fieldset if the box is checked.
    '#states' => array(
      'expanded' => array(
        ':input[name="expand_more_info"]' => array('checked' => TRUE),
      ),
    ),
  );
  $form['more_info']['feedback'] = array(
    '#type' => 'textarea',
    '#title' => t('What do you have to say?'),
  );

  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit your information'),
  );

  return $form;
}

Comments

nbie’s picture

In case I have two select list for e.g. state and city, where city list depends on selection of a value from state list. How do I do that with #states property?

bartl’s picture

If one of the fields depend visibility depends on the state of another element, and its "#required" parameter is set to true, is it possible to only have it required if the condition is met?

Since this "#states" thing only seems to apply to tricks using jQuery, thus, Javascript, I don't see how the built-in form validation in Drupal can take the state state into account.

The only way I can see around it is not make it really required, but

  1. add HTML to appen the Drupal red star to the field title, and
  2. in your custom form-validate routine, conditionally explicitly validate that a value for this field is present.

If I'm wrong, please tell me a better way.

bartl’s picture

Digging deeper, I found a comment thread about this exact same problem, here: http://api.drupal.org/api/drupal/includes--common.inc/function/drupal_pr...

It looks like the current state is indeed as I saw it. Which is... unfortunate. This "#states" thing is only a half solution to a fairly common problem.

I'll take a good look at the implementation of the module Conditional Fields (http://drupal.org/project/conditional_fields) to see if, and how, it solves this problem. (Yet, for Drupal 7, the maintainers still strongly dissuade using this module for production sites. Hence, I can't just use that module.)

Kristen Pol’s picture

I found too many bugs in Conditional Fields so had to remove it and add custom #states code.

dahousecat’s picture

The red star can be added to the field that you want to be required by setting the required property in the states array.

E.g.

'#states' => array(
	'visible' => array(
		':input[name=field_name]' => array('value' => 'your value'),
	),
	'required' => array(
		':input[name=field_name]' => array('value' => 'your value'),
	),
),

To actually perform the validation a custom validation function is required however I tried to automate the function by inspecting the #states array in the validation function so rules only have to be specific once, not twice. This does rely on the name of the element being in the jQuery selector in the format :input[name="field_name"] or it won't work.

This code is only tested in the specific scenario that I was using it in however I though it may prove useful to someone else.

function hook_form_validate($form, &$form_state) {
  // check for required field specified in the states array
  foreach($form as $key => $field) {
    if(is_array($field) && isset($field['#states']['required'])) {
      $required = false;
      $lang = $field['#language'];
      foreach($field['#states']['required'] as $cond_field_sel => $cond_vals) {
        
        // look for name= in the jquery selector - if that isn't there then give up (for now)
        preg_match('/name="(.*)"/', $cond_field_sel, $matches);

        if(isset($matches[1])) {
          
          // remove language from field name
          $cond_field_name = str_replace('[und]', '', $matches[1]);

          // get value identifier (e.g. value, tid, target_id)
          $value_ident = key($cond_vals);

          // loop over the values of the conditional field
          foreach($form_state['values'][$cond_field_name][$lang] as $cond_field_val) {

            // check for a match
            if($cond_vals[$value_ident] == $cond_field_val[$value_ident]) {
              // now we know this field is required
              $required = true;
              break 2;
            }
          }
        }
      }

      if($required) {
        $field_name = $field[$lang]['#field_name'];
        $filled_in = false;
        foreach($form_state['values'][$field_name][$lang] as $item) {
          if(array_pop($item)) {
            $filled_in = true;
          }
        }
        if(!$filled_in) {
          form_set_error($field_name, t(':field is a required field', array(':field' => $field[$lang]['#title'])));
        }
      }

    }

  }
}
GaëlG’s picture

For faster rendering of big forms, use IDs in your JQuery selectors : something like '#edit-field-name' instead of ':input[name=field_name]'.

sokrplare’s picture

Adding here for reference. This is per the thread over on Randy's site (http://randyfay.com/comment/2208#comment-2208)-

The only problem with that, of course, is that the ID must be unique on the page. And you can't be using AJAX in any of those elements (which would be strange anyway... one should generally use either #states or AJAX) because AJAX messes with the ID on every load.

In D7, the HTML ID changes on every request, due to drupal_html_id(). In practice, it's always the same on the initial page load, but after that, any part replaced by an AJAX action will result in an unpredictable ID (which will be different).

japo32’s picture

Perhaps a better way would be to combine selectors and use the ID of the form or the container of that form (i.e. "#myform :input[name=field_name]").

as noted in the jQuery API site:

Because :input is a jQuery extension and not part of the CSS specification, queries using :input cannot take advantage of the performance boost provided by the native DOM querySelectorAll() method. To achieve the best performance when using :input to select elements, first select the elements using a pure CSS selector, then use .filter(":input").

since we can't use the filter function, I'd say combining the selectors would be a good solution.

vebs0205’s picture

Thanks to developers for making it all so easy to use and understand. Even a non programmer can make effective use of all this to do big tasks easily!

Catsys’s picture

It turns out that we can only use AND. And how to implement eg the following condition:
See input "c" as long as the input "a" or input "b" are checked = true.

bucefal91’s picture

See here a reasonable idea on how to do it: https://api.drupal.org/comment/24708#comment-24708 Basically you'll have to apply some discrete math transformations to make your condition consist only of ANDs.

Anybody’s picture

Very very important note: Never forget to cast values to string, as the comment above already stated out:
https://api.drupal.org/comment/36623#comment-36623

Otherwise it will simply NOT work in Internet Explorer (for numeric values)!

I've written a blog article about this very dangerous bug:
http://julian.pustkuchen.com/en/drupal-7-fapis-states-take-care-internet...

Take care!

belthazorjk’s picture

I was searching for the OR condition and it's done this way.
Example if you have checkboxes with options 1, 2, 3, 4 and 5 and you want to set the visibility only when option 1 OR 3 is checked.

<?php
...
'#states' => array(
  'visible' => array(
    array( ':input[name="XXX"]' => array("checked" => TRUE) ),
    array( ':input[name="XXX"]' => array("checked" => TRUE) )
  )
),
...
?>
5418ryadav’s picture

Thanks. It is working.

leex’s picture

Applied states also apply their opposite, which is undocumented and unexpected.

For example, if you have 2 checkboxes, checkboxA and checkboxB and checkboxB should be unchecked when checkboxA is checked, checkboxB will also become checked when checkboxA is unchecked. Very frustrating.

See: http://drupal.stackexchange.com/a/136964/3936

martin_q’s picture

Deleted. Issue 2680529 filed instead.

dzy’s picture

$form['high_school_one']['sat_score'] = array(
               ............
       );
 $form['high_school_two']['sat_score'] = array(
               ............
       );

there two sat_score filelds, not working like this

'#states' => array(
    'visible' => array(
        ':input[name=high_school_two[sat_score]]' => array('value' => 'your value'),
    ),