Using CTools for form item dependency / visibility
What are CTools Dependencies?
This article was written in reference to Drupal 6. See below for notes about Drupal 7.
How to add visual dependencies to your form elements
An example. This code can live in the form builder callback, or a hook_form_alter callback:
// 1. Include CTools Dependent helper ctools_include('dependent');
$form['restrict_by'] = array ( '#type' => 'select', '#title' => t('Restrict by'), '#options' => array ( 'trip' => t('Trip'), 'month' => t('Month'), ), '#default_value' => $restrict_by_default_value, );
$form['trips'] = array ( '#type' => 'select', '#title' => t('Trips'), '#options' => array ( 'trip1' => t('Mediterranean Cruise'), 'trip2' => t('Trek to the North Pole'), ), '#default_value' => $trips_default_value,
// 2. This ensures that the element will be processed by ctools dependent '#process' => array('ctools_dependent_process'),
// 3. This instructs ctools_dependent which other elements this // should depend on. It has the following format: // #dependency => array('id-of-element-to-depend-on' => array('any_value', 'will', 'trigger', 'me')) '#dependency' => array('edit-restrict-by' => array('trip')), );
$form['months'] = array ( '#type' => 'select', '#title' => t('Month'), '#options' => array ( '1' => t('Jan'), '2' => t('Feb'), //...and so on ), '#default_value' => $months_default_value, // And likewise '#process' => array('ctools_dependent_process'), '#dependency' => array('edit-restrict-by' => array('month')), );
Depending on radios
For radios, because they are selected a little bit differently, instead of using the CSS id, use: radio:NAME where NAME is the #name of the property. This can be quickly found by looking at the HTML of the generated form, but it is usually derived from the array which contains the item. For example, $form['menu']['type'] would have a name of menu[type]. This name is the same field that is used to determine where in $form_state['values'] you will find the value of the form.
So, in this case our example becomes:
$form['restrict_by'] = array ( '#type' => 'radios', ... ); $form['trips'] = array ( ... '#dependency' => array('radio:restrict_by' => array('trip')), );
Things to watch out for / How to make this darn thing hide my checkboxes, radios, fieldsets & CCK widgets!
- Add the other #process callbacks In the current version of Form API, if you add a new item to a form and provide a #process for it, then it's default #process callback(s) will not be added. This can break checkboxes, radios and all CCK-provided widget elements. When you add #process to an element, first check what #process values it would get by default, and add those in addition to
ctools_dependent_process. Some common examples:
'#process' => array('ctools_dependent_process', 'expand_checkboxes')
'#process' => array('ctools_dependent_process', 'expand_radios')
- Are you trying to hide a fieldset? By default fieldsets do not get their #process callbacks run, so ctools_dependent_process does not get called. Set #input = TRUE on the fieldset item to force #process callbacks to run and fix this issue.
- Does the output have a -wrapper? CTools dependent assumes that the rendered output of the form item it is trying to hide is wrapped by a certain div:
...Rendered form item
Where ID is the #id of the dependent element. In simple cases you don't need to care about this, but if the form element does not conform, then dependent.js won't be able to find it and nothing will work. This affects checkboxes and radios among other form element types. The solution is to add the wrapper ourselves:
$form['trips'] = array ( '#type' => 'checkboxes', ... '#prefix' => '', '#suffix' => '', '#process' => array('ctools_dependent_process', 'expand_checkboxes'), '#dependency' => array('radio:restrict_by' => array('trip')), );
If a process / theme callback does not honour this #id, then ctools could be trying to use an id that is not actually rendered. To fix this issue it might be sufficient to inspect the HTML, determine the #id and set it manually on the element (FormAPI will use yours rather than generating one). Alternatively you may find that moving 'ctools_dependent_process' to the last item in the #process array fixes the problem.
If it still isn't working, the most comprehensive way around this problem is to specifiy your own id for the element, then wrap the element accordingly and make sure that ctools_dependent_process is the first listed #process callback:
// Foolproof dependency $form['trips'] = array ( ... '#dependency' => array('radio:restrict_by' => array('trip')), // 1. Specify an html id of our own devising. This overrides // anything FormAPI might generate '#id' => 'edit-trips', // 2. Manually wrap the whole thing with'#prefix' => '', '#suffix' => '', // 3. Make sure ctools_dependent_process is listed first '#process' => array('ctools_dependent_process', 'expand_checkboxes'), );
If you ever try to hide a hierarchical select element using ctools, you'll probably encounter this problem.
Dependencies In Drupal 7
Form element dependencies are much easier to create now, with support 'out of the box', working in a slightly similar way to these CTools dependencies, but without needing CTools! The
#statesform element property is the answer. Our article on dynamic forms in Drupal 7 goes into more detail. You can see this in action at d7.drupalexamples.info.
You can of course use CTools dependency in Drupal 7 - it's still here, and offers more flexibility with evaluation conditions - but the
#statessolution in Drupal core will be the answer for most people.