| 7 common.inc | drupal_process_states(&$elements) |
| 8 common.inc | drupal_process_states(&$elements) |
Adds JavaScript to change the state of an element based on another element.
A "state" means a certain property on a DOM element, such as "visible" or "checked". A state can be applied to an element, depending on the state of another element on the page. In general, states depend on HTML attributes and DOM element properties, which change due to user interaction.
Since states are driven by JavaScript only, it is important to understand that all states are applied on presentation only, none of the states force any server-side logic, and that they will not be applied for site visitors without JavaScript support. All modules implementing states have to make sure that the intended logic also works without JavaScript being enabled.
#states is an associative array in the form of:
array(
STATE1 => CONDITIONS_ARRAY1,
STATE2 => CONDITIONS_ARRAY2,
...
)
Each key is the name of a state to apply to the element, such as 'visible'. Each value is a list of conditions that denote when the state should be applied.
Multiple different states may be specified to act on complex conditions:
array(
'visible' => CONDITIONS,
'checked' => OTHER_CONDITIONS,
)
Every condition is a key/value pair, whose key is a jQuery selector that denotes another element on the page, and whose value is an array of conditions, which must bet met on that element:
array(
'visible' => array(
JQUERY_SELECTOR => REMOTE_CONDITIONS,
JQUERY_SELECTOR => REMOTE_CONDITIONS,
...
),
)
All conditions must be met for the state to be applied.
Each remote condition is a key/value pair specifying conditions on the other element that need to be met to apply the state to the element:
array(
'visible' => array(
':input[name="remote_checkbox"]' => array('checked' => TRUE),
),
)
For example, to show a textfield only when a checkbox is checked:
$form['toggle_me'] = array(
'#type' => 'checkbox',
'#title' => t('Tick this box to type'),
);
$form['settings'] = array(
'#type' => 'textfield',
'#states' => array(
// Only show this field when the 'toggle_me' checkbox is enabled.
'visible' => array(
':input[name="toggle_me"]' => array('checked' => TRUE),
),
),
);
The following states may be applied to an element:
- enabled
- disabled
- required
- optional
- visible
- invisible
- checked
- unchecked
- expanded
- collapsed
The following states may be used in remote conditions:
- empty
- filled
- checked
- unchecked
- expanded
- collapsed
- value
The following states exist for both elements and remote conditions, but are not fully implemented and may not change anything on the element:
- relevant
- irrelevant
- valid
- invalid
- touched
- untouched
- readwrite
- readonly
When referencing select lists and radio buttons in remote conditions, a 'value' condition must be used:
'#states' => array(
// Show the settings if 'bar' has been selected for 'foo'.
'visible' => array(
':input[name="foo"]' => array('value' => 'bar'),
),
),
Parameters
$elements: A renderable array element having a #states property as described above.
See also
form_example_states_form()
1 call to drupal_process_states()
File
- includes/
common.inc, line 4546 - Common functions that many Drupal modules will need to reference.
Code
function drupal_process_states(&$elements) {
$elements['#attached']['library'][] = array('system', 'drupal.states');
$elements['#attached']['js'][] = array(
'type' => 'setting',
'data' => array('states' => array('#' . $elements['#id'] => $elements['#states'])),
);
}
Login or register to post comments
Comments
Client-side vs server-side
As written earlier
> Since states are driven by JavaScript only, it is important to understand that all states are applied on presentation only, none of the states force any server-side logic...
OK, but is it possible somehow to at least disable errors on the hidden fields?
I have a form containing required fields (with '#required'=true, as it is easy to use and themed automatically, checked automatically). I want to use #states to hide parts of the form based on user selections and dependencies between form parts.
The client-side (presentation) works well, but after submitting the form I can't configure (or get rid of) the error messages on hidden and even disabled form fields which were #required.
A possible way would be to #limit_validation_errors on the submit button, but then I will loose the built-in "empty" check for all fields and there is no way to call it if needed.
Any suggestions for a normal approach, how to limit only parts of the '#required' validation decided on the server side based on some form values?
You could make those fields
You could make those fields as not required then use the validation callback to make those fields required. You would have to add something to the #value to denote that the field is required. I've done this as a workaround and it works quite nicely.
Another way to do it is, in
Another way to do it is, in your custom validate function, check all errors thrown on a field affected by a state, and then remove errors that should not be applied.
Something like this:
function custom_validate_function(&$form, &$form_state) {// Remove all error messages
drupal_get_messages('error');
// Get the array of errors and reset them
$errors = form_set_error(NULL, '', TRUE);
// Make sure that each error should be applied to the field
foreach (array_keys($errors) as $field) {
$value = $form_state['values'][$field];
// Perform some sort of validation on this value
if ($validation == TRUE) {
unset($errors[$field]);
}
}
// Add back all the errors that were not affected by our validation
foreach ($errors as $field => $message) {
form_set_error($field, $message);
}
}
This is not exactly right for Drupal 7
Thanks for the good start and pointing me the way but it does not quite work the way the code is written. The call to form_set_error
// Get the array of errors and reset them$errors = form_set_error(NULL, '', TRUE);
is returning a copy of the static array of the errors instead of a reference to the array since the function form_set_error does not return by reference. The calls to
unset($errors[$field]);only unsets the errors on the array copy. This prevents the form submit from happening because the original errors are still there. To get around this I had to do a hack to pull the actual static array into my validation function like so:
$errors = &drupal_static("form_set_error", array());Then the unset is actually affecting the real error array and the submit will go through. This does not seem like a good thing to do but I cannot find any other way to remove an error once it is set. I also had to explicitly set the error message again.
Does anybody else know a better way to do this? I am brand new to Drupal and writing my first module but this seems like it would be a pretty common thing to do using conditional '#states' on form fields.
For completeness my rewrite of the function above looks like:
function custom_validate_function(&$form, &$form_state) {// Remove all error messages
drupal_get_messages('error');
// Get the array of errors and reset them - CHANGED
$errors = &drupal_static("form_set_error", array());
// Make sure that each error should be applied to the field
foreach (array_keys($errors) as $field) {
$value = $form_state['values'][$field];
// Perform some sort of validation on this value
if ($validation == TRUE) {
unset($errors[$field]);
}
}
// Add back all the errors that were not affected by our validation
foreach ($errors as $field => $message) {
form_set_error($field, $message);
// ADDED
drupal_set_message($message, 'error');
}
}
How can I implement OR
How can I implement OR statement for remote conditions? For example, I need some form element to be visible when value of select is value1 or value2 and invisible in other cases. How can I do that?
Have the same problem. OR is
Have the same problem. OR is not yet supported: http://drupal.org/node/735528. I'll have to use #ajax instead, for now.
OR lists
I found one hack-around for OR lists, if you have a reasonably small number of possible values (you're going to have to list all the ones you don't want to select):
http://api.drupal.org/api/drupal/includes--common.inc/function/drupal_pr...
Two gotchas
Two gotchas that may save others some time:
1. Fields need to have the language in the selector or the name doesn't match
2. When comparing against a select's value that is a number, you need to cast the value to a string or it won't match (see http://drupal.org/node/879580)
An example of both:
if (isset($form['field_tohide'])) {// Get the name of the remote field with language
$lang = $form['field_term_location']['#language'];
$fhsname = 'field_term_location['. $lang .']';
// Set up the state rule so that we hide this field when the value is "3"
$form['field_tohide']['#states'] = array(
'invisible' => array(
':input[name="'. $fhsname .'"]' => array('value' => (string) '3'),
)
);
}
The above example is in the context of a hook_form_node_form_alter() function that hides one field (field_tohide for illustration purposes) when a certain term is selected in another field (in this example, a taxonomy select called field_term_location). First we need to get the language in the remove field (turning :input[name="field_term_location"] into :input[name="field_term_location[und]"]). The next thing to notice is that the value we are comparing to is "(string) '3'".
Always remember to add language to fields
Thank you for saving my time.
Field doesn't hide
Thx for this hint. But when I add the #states property to existing fields in hook_form_node_form_alter() the fields are always visible.
$form['field_teaser_image']['#states'] = array('invisible' => array(
':input[name="visibility"]' => array('value' => (string) '1'),
)
);
But the #states property works in the same form on normal form elements.
$form['title']['#states'] = array('invisible' => array(
':input[name="visibility"]' => array('value' => (string) '1'),
)
);
Any clues?
"Attribute Starts With Selector" and numeral values as strings
Attribute Starts With Selector
You can also query the DOM for INPUT fields with a name starting with 'field_term_location', this way you do not have to specify a language or delta.
<?php$form['field_tohide']['#states'] = array(
'invisible' => array(
':input[name^="field_term_location"]' => array('value' => (string) '3'),
),
);
?>
See http://api.jquery.com/attribute-starts-with-selector/
Values as strings
Internet Explorer needs numeral values as strings, but in our current project "'3'" seems to work, why the need of "(string) '3'"?
Question
Another question: does anyone know why these #states selectors always start with a colon? They also seem to work without the colon...
Radio Button - show/hide field
Not able to get a radio button option if checked to show and if un-checked to hide another form element. It will show if checked just fine but if another radio button option is selected in the group, the target element does not hide.
$form['field_request_subscribers']['#states'] = array('visible' => array('#edit-field-request-audience-und-selective' => array('checked' => TRUE),
),
);
Tried adding state behaviors on one of the other two radio button options to make the target element invisible if selected but that did nothing.
$form['field_request_subscribers']['#states'] = array('visible' => array('#edit-field-request-audience-und-selective' => array('checked' => TRUE),
),
'invisible' => array('#edit-field-request-audience-und-all' => array('checked' => TRUE),
),
);
Also tried
'visible' => array('#edit-field-request-audience-und-selective' => array('checked' => TRUE),),
'invisible' => array('#edit-field-request-audience-und-selective' => array('checked' => FALSE),
),
If the element is a checkbox vs a radio group it works just fine.
Any suggestions?
Radio buttons
If you are working with radio buttons or select lists you can't use 'checked' => 'true/false'.
Instead, you must use 'value' => 'name', where 'name' is the name of the radio button that you require to be selected before making your field visible/invisible.
I often forget this myself but it is mentioned at the top of the page.
When referencing select lists and radio buttons in remote conditions, a 'value' condition must be used:
<?php'#states' => array(
// Show the settings if 'bar' has been selected for 'foo'.
'visible' => array(
':input[name="foo"]' => array('value' => 'bar'),
),
),
?>
A gotcha with markup type form elements
Elements of type 'markup' don't respond to the 'visible' property.
See this blog post for more details and a workaround: http://www.bywombats.com/blog/06-25-2011/using-containers-states-enabled...
What does "may not" mean?
Does "may not" here mean, "might not" or does it mean "must not"?
In other words, if someone tries to use these states, is it expected that nothing will happen or is it expected that something might happen?
Conditional #options ?
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.
Another great blog post about
Another great blog post about the states system: http://randyfay.com/states
Multiple required tags!
I'm trying to make a field optional upon condition of a radio button selection, like so:
$form['body']['#states'] = array('optional' => array(
':input[name="hero_type_selection"]'
=> array('value' => 'public_figure'),
),
);
The problem is, any time I select a radio button that is NOT "public_figure," the required tag is added back to the body field, even if it's already there. The result is something like this:
Is there any way to get it to stop adding more and more asterisks?
EDIT: It turned out I simply had an extraneous Drupal.attachBehaviors() in my jQuery include, so this is working fine as-is!
Negated values and OR lists
I was having trouble finding a way of getting a form field (the cardinality field on a field edit form) hidden if a select widget had any one of a number of values, essentially I wanted to specify the element should be invisible if value IN (x,y,z). Ideally we'd need OR operations implemented for values to do this, but #states are always an AND list of conditions (as stated above) ... The inverse (visible), has the same issue - I'd want to specify visible if value IN (a,b,c), and if there's no NOT operator for values, then I couldn't do visible if value != a AND value != b AND value != c.
However, as it happens, It appears to be the case that you can use "!value" to denote value != (not mentioned above). So that gets us almost able to use an OR lists equivalent rewritten as an AND ... except for the fact that you can still only specify one value for each key - a repeated JQUERY_SELECTOR will overwrite a preceding one, as the array is keyed off the JQUERY_SELECTOR.
But ... for the sake of getting the array key unique, you can add some random unique nonsense to the end of it so long as it's syntactically correct and wouldn't actually match anything, so I ended up with something like this to implement invisible if value IN (x,y,z) - assume here the full set of possible values is (a,b,c,x,y,z):
<?php$all = array('a', 'b', 'c', 'x', 'y', 'z');
$visible = array('a', 'b', 'c');
foreach (array_diff($all, $visible) as $i => $vocab) {
$form['foo']['#states']['visible'][':input[name="bar"], my-nonsense-dummy-element' . $i] = array('!value' => $vocab);
}
?>
Hopefully this may be of use to someone, or might inspire someone to point out a better way of doing this... Or maybe it's time to make it possible to specify OR lists for values.
Awesome trick
@Alan Evans this is an awesome trick, thanks for posting it!
Empty Radio Selection
I fought with #states a bit as I only wanted to show an element if a group of radios was not yet selected, since 'empty' did not seem to work on a group of radios. However, this did:
'#states' => array('invisible' => array(
':input[name="trial_day"]' => array('!value' => false),
),
),
Note: Another gotcha. 'false' must be in lowercase, or it won't work in IE.
This may also work with selects, but I haven't tried.
#states in add to cart form
HI! I have used #states to display an attribute text field if a corresponding in the add to cart works fine in node full view but when viewing multiple nodes in single page with a teaser view ....it doesn't work why?
Note that "OR" lists were
Note that "OR" lists were just committed to D7 and will be available in the next release (7.13): http://drupal.org/node/735528
OR with radio buttons
Something else that worked for me with radio buttons:
if the options for field_one are 1, 2, 3, 4 and field_two should be visible if option 3 or 4 is selected the code looks like this:
<?php$form['field_two']['#states'] = array(
'invisible' => array(
':input[name="field_one[und]"],value="3"' => array('!value' => (string)'3'),
':input[name="field_one[und]"],value="4"' => array('!value' => (string)'4'),
),
);
?>
The html code for reference:
<input type="radio" id="edit-field-one-und-1" name="field_one[und]" value="1" class="form-radio" />