Using containers as #states enabled markup form elements

As part of the sprint we're holding in Paris right now to introduce new Commerce Guys to Drupal Commerce development, we devised a situation where we wanted a conditionally available message on the checkout form. We decided for a shipping scenario that we wanted to present a message to the user regarding shipping costs inline with the address form if the customer selected a shipping address outside of the free shipping country of the store.

Adding a random text message to a form is trivial. You can just add a markup element to the form, using a div tag to make sure it ends up in a fieldset if necessary:

<?php
$form
['message'] = array(
 
'#markup' => t('What a wonderful message!'),
);
?>

Additionally, adding a #states array that includes the instruction to hide a form element on the basis of a select list's value is trivial:

<?php
$form
['message'] = array(
 
'#markup' => t('What a wonderful message!'),
 
'#states' => array(
   
'invisible' => array(
     
':input[name="select_list_name"]' => array('value' => 'US'),
    ),
  ),
);
?>

Unless I botched my pseudo code, that #states array should instruct the Form API to include JavaScript to hide the element when the select list I specified has a value of US. Unfortunately, that code won't work. Shocked

The problem is that the JavaScript that hides the element depends on the element's ID to target the behavior, and a markup element by default gets no wrapping. This means you can't directly put a #states array on a markup element in general. You do have a few options to work around this, and I'll end with what we went with in our case... you can tell me if we're crazy. Sticking out tongue

  1. You could just hardcode a div wrapper that includes the ID and the form-wrapper class Drupal expects, but I can't say that I'd recommend it. Names change quite frequently during development and can easily be altered.
  2. You can also just put the markup element inside a container element and attach the #states array to the container. Containers are rendered as divs with proper IDs for targeting, so this works just fine.
  3. However, we wanted a compact solution, so what we did is turn the markup element into a container element and set its #children property to the message. This effectively makes the container element function as a markup element, but it actually wraps the markup in the appropriate div on output. Since #children is already set, this does mean that the container element cannot actually contain other elements, so there may be good reasons for you not to try this at home.

The gist of our solution was:

<?php
$form
['message'] = array(
 
'#type' => 'container',
 
'#children' => t('What a wonderful message!'),
 
'#states' => array(
   
'invisible' => array(
     
':input[name="select_list_name"]' => array('value' => 'US'),
    ),
  ),
);
?>

I suppose it would be nice if we didn't have to abuse the #children property name, but this seems like fair game to me unless it's a possibility to change the markup element to include the div and expected ID.

Topics: 

Comments

where you are in Paris with your wife and child instead of all alone? =D

And abusing children is always bad... property names or not! Even a non-programmer like me knows that.

I miss you. E does too!

--Christina

hehe I saw the same thing in my last sentence but decided not to play it up. Wink

"container" type and "#children" key are not documented in http://api.drupal.org/api/drupal/developer--topics--forms_api_reference....

Funny, I didn't realize that. Container functions in the Forms API like a fieldset as far as #tree and children are concerned, but it just renders as a div with no label instead of as a fieldset. I use it in Commerce Checkout to allow site admins to choose between using a fieldset or a div wrapper for elements of the drag-and-drop checkout form.

Why don't you just use #type' => 'item'?

I noticed that in the docs when I wrote this post, too. I suppose that would work just fine - I didn't need the title, but I'd hazard a guess that without the title property the element would be rendered the same as the container.

Yes, you are right, you can leave out the #title attribute. You could code it like this, I think:

<?php
$form
['message'] = array(
 
'#type' => 'item',
 
'#prefix' => '<div id="div_name">',
 
'#suffix' => '</div>',
 
'#markup' => t('What a wonderful message!'),
 
'#states' => array(
   
'invisible' => array(
     
':input[name="select_list_name"]' => array('value' => 'US'),
    ),
  ),
);
?>

There are some similar examples in the form example module (form_example_states.inc).

The invisible selector you have passed won't work, will it?