Recently, I was creating a form that provided a list of options as checkboxes and needed to include helper text for each individual checkbox. While the Form API in Drupal 7 has a #description
attribute, for checkboxes and radios it applies that as text for the entire group. After a lot of looking, there didn't seem to be a way that allowed for passing descriptions into each item in the #options
array that is expected. I discovered the FAPI's #after_build
callbacks, which turned out to be just the solution I needed.
Defining an #after_build
callback
The FAPI documentation describes #after_build
pretty well: “An array of function names which will be called after the form or element is built.” This allows us to make further alterations to our form elements after they're processed by Drupal, but before they're rendered to HTML. In the case of checkboxes and radios, the FAPI elements for each individual checkbox/radio are inserted by Drupal during the initial form build. That means in order to alter those specific elements, we'll need to access the form array after the initial build. Hence, #after_build
.
To get started, add some #after_build
callbacks to your element. This can be done either through a new form element definition (as shown below), or by adding new elements to #after_build
through hook_form_alter()
.
<br /> $form['form_checkboxes'] = array(<br /> '#type' => 'checkboxes',<br /> '#title' => t('My Checkboxes'),<br /> '#options' => array(<br /> 'option_1' => 'Option 1',<br /> 'option_2' => 'Option 2',<br /> ),<br /> '#options_descriptions' => array(<br /> 'option_1' => 'Description 1',<br /> 'option_2' => 'Description 2',<br /> ),<br /> '#default_value' => variable_get('form_checkboxes', array()),<br /> '#after_build' => array('_option_descriptions'),<br /> );<br />
This will call a function named
_option_descriptions
, and pass it arguments for the $element
and the $form_state
that we can make alterations on. Some of you might have noticed the #options_descriptions
key, which isn't in the FAPI documentation. We'll get to that shortly, but first let's look at our callback function.
Custom #after_build
callback with #options_descriptions
All functions defined in #after_build
are expected to return the $element
argument after we've made our changes. As mentioned earlier, #options_descriptions
isn't a standard FAPI option. Instead, it's a custom convention we'll use that our callback function will expect. Each option in the #options
array shares a key with the items in the #options_descriptions
array. That shared key is the glue between each option and its description. Writing our custom callback to take advantage of this, we end up with:
<br /> /**<br /> * Provide a description to each option in the element<br /> */<br /> function _option_descriptions($element, &$form_state) {<br /> foreach (element_children($element) as $key) {<br /> $element[$key]['#description'] = t('!description', array(<br /> '!description' => $element['#options_descriptions'][$key],<br /> ));<br /> }</p> <p> return $element;<br /> }<br />
While we only needed a couple lines of code, let's look into what they do. First we're looping over the results of our
$element
passed into Drupal's element_children()
function. This looks over our $element
array for any children, or any key that doesn't start with #
(signifying that it's renderable). In the case of our checkboxes, this becomes each individual checkbox. This function returns their keys as an array that we can then loop over.Next we alter the render array for each checkbox, adding a
#description
to it and assigning it the description with the shared key in our #options_descriptions
array. Our final checkboxes now look like this:
Since each input is a renderable item, and we're leveraging Drupal's #description
element for them, everything gets passed through the same theme hooks you'd expect for Drupal forms. These descriptions can get templated, preprocessed, or altered as you would anything else.
Success!
This example focused on checkboxes, however, this code works the same for groups of radio buttons as well. It's worth mentioning that this seems to be something that already works in Drupal 8, although it isn't well documented yet.