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.

33 Comments

What about people who have

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'

All is shown if javascript is disabled

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.

Try Ctools dependencies instead

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

I am no drupal expert. And

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?

Answer in case anyone else needs help.....

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

Double Quotes Needed

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),

Thanks - updated

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

How to depend on multiple fields properly?

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?

Preprocess form question

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 :)

Depending on non-form items

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?

Probably not

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.

What about GUI forms?

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?

I'm not familiar with any way

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.

Conditional #options ?

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.

Don't forget about #ajax and multistep forms

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.

Great Suggestion!

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

Thanks

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.

Performance

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

Requires uniqueness, and no AJAX

Thanks. 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.

Huh?

What do you mean AJAX messes with the ID? Changes the primary key on every request?

HTML ID changes on every request

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).

Dynamic forms doesn't work

Hi, I wrote this code, it seems right but my dynamic form doesn't work. When page loads (it loads inside a popup) all fields are shown. My hook_menu calls ripa_slideshow_edit_page() to show only the form without entire page.
I tried several variants but nothing seems work

function ripa_slideshow_edit_page(){
$output = drupal_get_form('ripa_slideshow_edit_form');
print render($output);
}

[edited out]

Resolved

I resolved the problem. When you call print render(...) function drupal stops its workflow and prints page immediatly so no js files are included and dynamic forms don't works.

uncheck checkbox programmatically

States property is great but I've found a little problem. Let's say we have a radio buttons like 'Cars' and 'Air Planes'. If 'Cars' button is checked another two checkboxes shows like 'Ferrari' and 'Mercedes'. If 'Air Planes' is checked also two different checkboxes are shown, it's 'Concorde' and 'Boeing'. Simple right? Now, lets say that user decided to choose Cars and then checks 'Ferrari'. After that our user changes his mind and checks 'Air Planes' without unchecking previous 'Ferrari'. And here is the problem, now user can check 'Boeing' and click 'Submit', however our form remember a 'Ferrari' value and 'Boeing' value which is incorrect. The purpose of choosing radio buttons is to give user one option to choose (OR) not many (AND). Does anyone knows how to uncheck 'Ferrari' checkbox automatically/programmatically at the momment when user chooses 'Air Planes'?

Difference between choosing one or many

Ferrari, Mercedes AND Concorde, Boeing should belong to the same radio group, all the four. This way the user may choose only one, whilst the #states only _helps_ her choosing the right one. Remember: #states is client side stuff, so cannot be relied upon. In other words, #states is for display/UX purposes, not logic (at least as I see it).

Signup Module

I tried the states form entries in a signup form, unfortunately it did not work. Do you have any idea on how to embed conditional fields in the signup module? I see the they us a standard $form entry for settings like "Phonenumber" etc so it shouldn't be to difficult.

Any help is appreciated

Select all

Hi Randy. I'm trying to use this to achieve the functionality of having a checkbox that, when selected, causes a range of checkboxes to all be selected. I made the selectall box a separate form field, and it looks like this, and works not a lick:
$form['report_group']['selectall'] = array(
'#type' => 'checkbox',
'#default_value' => 0,
);
$form['report_group']['boxes'] = array(
'#type' => 'checkboxes',
'#options' => $boxes,
'#states' => array(
'checked' => array(
':input[name="report_group[selectall]"]' => array('checked' => TRUE),
),
),