Author image
Senior Developer

Using CTools for form item dependency / visibility

What are CTools Dependencies?

One of several helpers included in the ctools module, ctools dependency is described on the module page itself as "a simple form widget to make form items appear and disappear based upon the selections in another item". It's designed to make it easy and quick to hide/show form elements based on the value of other form elements in the browser using javascript.

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

From ctools/includes/dependent.inc:

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!

  1. 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:
    1. checkboxes: '#process' => array('ctools_dependent_process', 'expand_checkboxes')
    2. radios: '#process' => array('ctools_dependent_process', 'expand_radios')
  2. 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.
  3. 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:
      <div id="ID-wrapper"> 
        ...Rendered form item
      </div>
    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' => '<div id="edit-trips-wrapper">',
      '#suffix' => '</div>',
      '#process' => array('ctools_dependent_process', 'expand_checkboxes'),
      '#dependency' => array('radio:restrict_by' => array('trip')),
    );
  4. Beware of #process or theme callbacks that disregard or change the #id of a form item Just before process callbacks are executed by FormAPI, the unique ID of the element is generated and set to $element['#id']. Theme callbacks use this value later on to render id attributes. When ctools_dependent_process is called it grabs this #id and passes it to javascript in good faith.

    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 <div id="#id-wrapper">
      '#prefix' => '<div id="edit-trips-wrapper">',
      '#suffix' => '</div>',
      // 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 #states form 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.

If you want to use AJAX, dynamically altering forms has been made easier than ever too. Given the advantages to handling form item dependencies on the server, rather than through javascript (e.g. consistent experience for js and non-js users, ability to customize the dependence beyond just visibility), consider familiarizing yourself with how its done and choosing to handle visibility dependencies via ajax.

You can of course use CTools dependency in Drupal 7 - it's still here, and offers more flexibility with evaluation conditions - but the #states solution in Drupal core will be the answer for most people.

Related pages

Comments

You alluded to this, but here is a specific warning for drupal 7. In some cases the form field ID's change from one rendering to the next. Since last I checked in ctools you can only specify the ID as a dependency so this doesn't always work. Hopefully in a future version of ctools you can specify the field name which is how d7 form states handle it. This will also help alleviate the problem with having to add wrappers.

Also one thing that form states in d7 doesn't handle that ctools does is dependency on more than one select item. Let's say you have a field that depends on a select. The select values are red, green, blue. If your field depends on the select being either red or blue then this can't be done in the current version of form states. Ctools dependency can handle this though.

Comments on this article are now closed, if you want to give us feeback you can use our contact form instead.