Drupal 7 FAPI's #states: A Great New UI Improvement For Forms

The little-known #states feature has gone into Drupal 7, and it rocks.

Before you read on, try this dynamic form live at d7.drupalexamples.info. It's developed without using a line of javascript, just plain Form API.

Essentially, you can provide dynamic behavior in a form based on changes to other elements in the form. An easy example: Often you only need to collect information if a particular element is selected. If they select type=student, you don't have to require them to fill in a further "Employer" field.

The new #states example in the Examples module's Form Example shows how a dynamic form can work. You can try it out live as well at d7.drupalexamples.info.

The idea of #states is that you add a #states property to a form element that is supposed to change when some other form element changes. So if a form element is supposed to be shown or hidden, the #states property will be added on that element, not on the element that caused the change.

The #states property is a structured array of action => condition arrays. The action is 'visible' or 'checked' or 'required' or several other options. The condition is an associative array of 'jquery_selector' => array(value_statement). But mostly you can do it by copying and pasting examples. Much of the time the jquery selector can be ":input[name=field_name]" and the rest of the array can be cookbook from example code.

In the example, the 'tests_taken' field is only to be visible if the form-filler is a high school student:
  $form['tests_taken'] = array(
    '#type' => 'checkboxes',
    '#options' => drupal_map_assoc(array(t('SAT'), t('ACT'))),
    '#title' => t('What standardized tests did you take?'),
    '#states' => array(
      'visible' => array(   // action to take.
        ':input[name=student_type]' => array('value' => t('High School')),
      ),
    ),
  );

So we set the action to 'visible' when the condition (our select called student_type is set to 'High School') is true.

That's basically all there is to it.

So when would you use #states as opposed to AJAX forms or a sprinkling of jquery?

  • If your form actually changes under the hood based on user input, then you need to use AJAX forms. #states doesn't fundamentally change any of the elements, it just changes their presentation.
  • If you don't change the form, but need a transformation that simple conditional logic can't do, you'll have to roll some jQuery.

For more detailed comments and possibilities, read the #states example.

21 comments

by tstoeckler on Tue, 2010-04-13 02:14

What about people who have JavaScript disabled?
Will the above example never be shown, or will the whole #states thing simply be ignored?
If the former, I guess it would be better to use eg 'invisible' than 'visible'

by rfay on Tue, 2010-04-13 08:12

If javascript is disabled, the whole form is shown. It's as if the #states properties didn't exist (because they essentially don't).

So the form works, but you might need extra text and might want to use fieldsets to organize the data, for example.

by Wim Mostrey on Tue, 2010-04-13 08:48

Or try this Drupal 6 module conditional fields which allows you to create these forms without a single line of javascript or php code! In a couple of clicks you set up the perfect cck form.

by Dave Reid on Wed, 2010-04-14 11:08

Looks like Conditional Fields can only be used with CCK. States can be used with any Form API elements.

If you want #states type things in Drupal 6, I'd much rather recommend Ctool's 'Dependent' system which is what was shaped in the states system in D7.

http://drupal.org/project/ctools

by Kyle on Sun, 2011-08-21 19:00

I am no drupal expert. And before finding this page, i did try conditional field on D7. But I don't think conditional field can check whether a field's value equal to "High school" or not. Am I right?

I'm trying to use the #state attribute in my form with a tree=TRUE fieldset. How do I set ':input[name=]' when there is a treed fieldset?

Thanks

I found the answer through experimentation in case this helps anyone else:

If you are using a fieldset in front of your elements, then you must account for this in your jQuery selector. For example, if you have a fieldset such as 'ask' with a number textfield you want to expose when some other textfield is filled.

$form['ask']['number'] = array(
'#type' => 'textfield',
'#options' => ...
'#states' => array(
'visible' => array( // action to take.
':input[name=ask[so_no]]' => array ('filled' => TRUE),
),

In the input selector you must account for the fieldset by prefacing the name with the fieldset as shown above. Perhaps Randy can add this tidbit to the documentation at http://api.drupal.org/api/drupal/developer--topics--forms_api_reference.....

HTH

by IntoTheWoods on Thu, 2012-01-19 22:18

The input selector needs double quotes around the name value to work:

':input[name="ask[so_no]"]' => array ('filled' => TRUE),

For a single checkbox, it would be

':input[name="ask[so_no]"]' => array ('checked' => TRUE),

by JohnAlbin on Mon, 2010-07-05 22:15

The link to the examples needs to be updated to: http://d7.drupalexamples.info/examples/form_example/states

by rfay on Mon, 2010-07-05 22:38

Thanks - I just updated and think I got all instances.

Having three fields on a form: year-from, year-to and a title, all textfields. The title field should be enabled if and only if either of the year fields is filled, and having considered that Form API's #disabled attribute cannot be used for this. So I've came up with this:

<?php
function mymodule_myform() {
 
$form = array();
 
$form['yearfrom'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Year from'),
  );
 
$form['yearto'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Year to'),
  );
 
$form['title'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Title'),
   
'#states' => array(
     
'enabled' => array(
       
'#edit-yearfrom' => array('filled' => TRUE),
       
'#edit-yearto' => array('filled' => TRUE),
      ),
     
'disabled' => array(
       
'#edit-yearfrom' => array('empty' => TRUE),
       
'#edit-yearto' => array('empty' => TRUE),
      ),
    ),
  );
}
?>

This ensures that the form comes up with all the three fields being empty, and the title field being disabled (by jQuery, only on client-side). This ensures that when either of the year fields has anything in it, the title field becomes enabled. Speaking CS: both the enabler and disabler conditions are OR'ed together, but the disabler ones should be AND'ed.

But this solution also has a problem: if either of the year fields gets empty, the title field gets disabled, though it should be only getting disabled if both the year fields become empty. How should I address this?

In other words: the above example is quite good for an OR relation, but how to implement an AND relation between the (disabler) conditions?

by rfay on Sun, 2011-03-27 20:01

Answered over on http://drupal.org/node/1106388#comment-4269336.

I don't think I can help you do OR. If you have other questions, please provide a sample module (perhaps in a sandbox?)

by Maestro on Sun, 2011-07-24 15:23

I'm trying to add state in the preprocess form function. Just for testing.

function karapuziki_preprocess_recipe_node_form(&$variables) {

$variables['form']['make_link'] = array(
'#type' => 'checkbox',
'#title' => 'Make link',
);

$variables['form']['in_new_window'] = array(
'#type' => 'checkbox',
'#title' => 'in new window',
'#states' => array('visible' => array(
'input[name="make_link"]' => array('checked' => TRUE),
)),
);
}

This new two fields appear in the form, but both of them are visible and #states conditions doesn't work. I tried to add states conditions to field that already exist in the form

$variables['form']['recipe_source']['#states'] = array(
'visible' => array( // action to take.
':input[name=recipe_yeld]' => array('filled' => TRUE),
),
);

but #states doesn't work too. May be it can't work if I use preprocess hook? Please give me advice :)

by Boris Gordon on Wed, 2011-08-10 23:15

Is it possible to have a form element's state depend on value of a non-form element with #states or is this a custom jQuery job?

by rfay on Thu, 2011-08-11 08:01

I since the dependency is just a jquery target, you could... EXCEPT: The states that you can depend on are all form element states. I wouldn't be surprised if you could depend on the existence of some element though. Give it a try.

by Pete on Wed, 2011-09-21 15:54

I create forms using the webform user interface rather than code. Is there any way to add #states in the GUI? Any examples out there?
Alternatively, is there any way to take my GUI form and extract the Drupal code from it?

by rfay on Wed, 2011-09-21 16:07

I'm not familiar with any way to use #states in webform module, but I don't use it often. I'm quite sure that webform will not generate a module for you. But these would be good support issues in the Webform issue queue.

by Ben McFerren on Mon, 2011-10-24 19:00

Great write up on how to make the visibility of fields conditional! Many thnx for the clarity.

I am searching for a solution on how to make particular checkbox options conditional so that:

If user selects checkbox A in Field (i), a certain array of checkbox selections in Field (v) are visible. Additionally, if user also selects checkbox B in Field (i), a different array of checkbox selections is visible as well in Field (v).

I have a few taxonomy vocabularies (iii), one of which (x) has a few term reference fields that are populated with values derived from the additional vocabularies (iii).

In my "add node" form, I am trying to present the (iii) taxonomy fields at the top, then have the (x) taxonomy field below. I'd the checkbox selections in field (x) to be dependent upon the choices made previously in the (iii) taxonomy fields above.

Thnx in advance for your time and consideration.

by rfay on Mon, 2011-10-24 20:06

Although #states is great for lots of things, it's not as expressive as PHP, so you might have to use #ajax or mutistep forms to get what you want.

I don't think you'll be able to say "If A then one-set-of-things, if B then another-set-of-things, if A AND B, a third-set.

But you can easily make two sub-fieldsets, and everything is easy.

by Ben McFerren on Fri, 2011-11-11 20:37

Hi Randy - I greatly appreciate you pointing me in the right direction!

After much tinkering, I was able to get the #ajax to perform the first iteration of what I'm looking for.

I've posted it here for anyone to use or improve:

http://pastebin.com/2YPCyPYK

Now I am working on making the entire visibility of the topblock fieldset (and the entire topic field) contigent on whether checks have been made in the previous fields. I will keep you updated on my progress.

Many thnx for your help and guidance.

-Ben

by rfay on Fri, 2011-11-11 20:53

Thanks! Hey - for more permanence (somebody might come by and want to know what you did in a year or so) you might want to post your code on http://gist.github.com or as a sandbox on drupal.org, or something like that. Keeping your code in a repository is good for you too, as it helps you find things that you know you did sometime.

Drupal theme by Kiwi Themes.