Extending Form API #states with regular expressions

#states is a new Form API property in Drupal 7. It makes it easy to change the state (unchecked, visible, enabled, etc.) of an element based on the state of another element. Randy Fay has a great explanation of what it is and how to use it.

For example, to show a settings field only when a checkbox is checked, you can write

<?php
$form
['settings'] = array(
  
'#type' => 'textfield',
  
'#states' => array(
    
'visible' => array(
      
// Element to check => Condition to check
      
':input[name="toggle_me"]' => array('checked' => TRUE),
    ),
  ),
);
?>

It’s an amazing way to make forms interactive without writing any custom Javascript. However, it’s pretty limited in the things you can use as a dependency: you can condition on checkboxes being checked/unchecked, values being empty/nonempty, or a text/select box having a certain value.

In a recent project, we needed to display a ‘Postal/Zip code’ field if a select box had ‘Canada’ or ‘USA’ selected. For now, #states doesn’t support ‘OR’ in Drupal 7. We could have written some custom javascript, but then we thought “well, what if #states supported regular expressions?”



Source: http://xkcd.com/208

#states doesn’t support regular expressions out of the box. However, Konstantin Käfer’s slides (skip to #59) explain how to extend #states so that it does! Add the following code snippet to a suitable javascript file.

if (Drupal.states) { // Don't crash if states.js hasn't loaded yet
    // Override the 'Object' comparison in states
    Drupal.states.Dependent.comparisons.Object =
        function(reference, value) {
            if ('regex' in reference) {
                return (new RegExp(reference.regex, reference.flags)).test(value);
            } else {
                return reference.indexOf(value) !== false;
            }
        }
}

If your form element ever loads via AJAX, this snippet won't work as is: you need to put it inside a Drupal behavior. You’ll need the Behavior Weights module for this trick to work: this behavior needs to run before the Drupal.behaviors.states behavior.

Drupal.behaviors.statesModification = {
  weight: -10,
  attach: function(context, settings) {
    if (Drupal.states) {
      Drupal.states.Dependent.comparisons.Object =
        function(reference, value) {
          if ('regex' in reference) {
               return (new RegExp(reference.regex, reference.flags)).test(value);
          } else {
            return reference.indexOf(value) !== false;
          }
        }
    } 
}

Now all we need to do is write:

<?php
$form
['#attached']['js'][] = 'your_javascript_file.js';
$form['settings'] = array(
  
'#type' => 'textfield',
  
'#states' => array(
    
'visible' => array(
      
':input[name="country"]' => array('value' => array('regex' => '^(Canada|USA)$')),

    ),
  ),
);

?>

You could also modify this a bit to extend #states in other ways: to check to see if a value is in a list of values (Monkey, Chimpanzee, Gorilla...), for example. Note that since this overrides Drupal.states.Dependent.comparisons.Object, you'll need to do all your extending in the same place.