Drupal: Dependent dropdowns with jQuery

I have told my co-workers on many occasions that Drupal doesn’t do true Ajax. With true Ajax, elements change without a page refresh and only the relevant elements are affected. With Drupal, the elements change because the entire form is refreshed. For instance, if you use Drupal’s Ajax to make the values of one dropdown dependent upon the selected value of another, the entire form has to be refreshed in order for the second dropdown to be updated (See Ajax Example: Dependent Dropdown). Although, there are times when I do use Drupal’s Ajax in this scenario, I much prefer to use jQuery.

Below is a typical scenario – a form with multiple dropdowns.

function my_module_dropdown_example_form($form, &$form_state) {
  drupal_add_js(drupal_get_path('module', 'my_module') . '/js/my_module.js', array('group' => JS_DEFAULT));
 
  $optionsFirst = getFirstOptions();
  $optionsSecond = getSecondOptions();
  $form['field_one_reference'] = array(
    '#type' => 'select',
    '#title' => t('First Dropdown'),
    '#options' => $optionsFirst,
  );
  $form['field_two_reference'] = array(
    '#type' => 'select',
    '#title' => t('Second Dropdown'),
    '#options' => $optionSecond,
  );
}

With a node-based form, the options for the dropdowns are filled automatically (like with a term_reference field), but in this case we manually fill each option with imaginary data. We’ve also added a Javascript file. In the case of a node-based form, we would use a hook_form_alter to add the Javascript file.

function my_module_content_type_node_form_alter($form, &$form_state) {
  drupal_add_js(drupal_get_path('module', 'my_module') . '/js/my_module.js', array('group' => JS_DEFAULT));
}

In some situations, it is better to add the Javascript in the node form’s after_build option. At times, there have been some unintended consequences when adding drupal_add_js directly in a form alter – although, not all time.

function my_module_content_type_node_form_alter($form, &$form_state) {
  $form['#after_build'][] = '_my_module_load_form_alter_js';
}
 
function _my_module_load_form_alter_js($form, &$form_state) {
  drupal_add_js(drupal_get_path('module', 'my_module') . '/js/my_module.js', array('group' => JS_DEFAULT));
  return $form;
}

Next, we create the Javascript file. We will use jQuery’s $.getJSON function to retrieve options for the dropdown Ajaxally.

?View Code JAVASCRIPT
(function ($) {
  Drupal.behaviors.initFormAlter = {
    attach: function (context, settings) {
    $(‘#edit-field-two-reference’).hide();
    var v = $('#edit-field-one-reference', context).val();
    if (v != ‘_none) {
      getFieldTwoOptions(v);
    }
 
    $('#edit-field-one-reference', context).change(function(){
      var v = $(this).val();
      if (v != '_none') {
        getFieldTwoOptions(v);
      }
    });
 
    function getFieldTwoOptions(v) {
      $.getJSON( '/my_module/get-field-two-options/'+v, function( data ) {
        if (data.length > 0) {
          $('#edit-field-two-reference', context).append('<option value="_none">- None -</option>');
          $.each( data, function( i, itm ) {
            $('#edit-field-two-reference', context).append(
              '<option value="'+itm.nid+'">'+itm.title+'</option>'
            );
          });
          $('#edit-field-two-reference', context).show();
        } else {
          $('.form-item-field-two-reference', context).append(
            $('<div></div>').attr('id', 'field-two-reference-message').html('There are no values for the selected field one option.')
          );
        }
      });
   }
})(jQuery);

So typically, in a node-based form, the dropdowns contain all values. For instance, if field-one and field-two are taxonomy terms attached to the node, both dropdowns contain all terms for the relevant taxonomy. But I only want the second dropdown to show terms that are related to the selected value of the first dropdown. So I generally hide the second dropdown until a value is selected in the first dropdown.

When the value of the first dropdown is changed, I use $.getJSON to retrieve the values and then fill the second dropdown. At this point the path that is used in the $.getJSON function is not known to Drupal, so we have to create it in a hook_menu.

function my_module_menu() {
  $items = array();
  $items['my_module/get-field-two-options/%'] = array(
    'page callback' => 'my_module_get_field_two_options',
    'type' => MENU_CALLBACK,
    'access callback' => TRUE,
    'page arguments' => array(2),
  );
  return $items;
}

When the $.getJSON function sends the selected value of the first dropdown to the specified url, the my_module_get_field_two_options function is called. This function will query the database to retrieve the options for the second dropdown.

function my_module_get_field_two_options($id) {
  $array = array();
  $select = db_select('node', 'n');
  $select->join('field_data_field_one_reference, 'o', 'o.entity_id = n.nid');
  $select->join('field_data_field_two_reference, 't', 't.entity_id = n.nid');
  $select->fields('n', array('nid', ‘title’));
  $select->condition('n.type', 'my_content_type');
  $select->condition('o.field_one_reference_nid', $id);
  $results = $select->execute();
  foreach ($results as $result) {
    $array[] = array('nid' => $result->nid, 'title' => $result->title);
  }
  echo drupal_json_encode($array);
}

Note that I echo the result, rather than use return. I found this to be the case when using Ajax in any PHP-based technology (Magento, Zend Framework, etc.). It is also important to note that for a node-based form, the nid for each value must be returned.

?View Code JAVASCRIPT
$('#edit-field-two-reference', context).append(
  '<option value="'+itm.nid+'">'+itm.title+'</option>'
);

The nid for the value is stored in the database.

If these were a taxonomy dropdowns, the query would be a bit different (I’ll also write it using db_query rather than db_select, but the result will be the same):

$array = array();
 
$sql = 'select o.entity_id,t.name from field_data_field_one_reference o
          join taxonomy_term_data t on t.tid = o.entity_id
          where o.field_one_reference_tid = :tid order by t.weight, t.name';
$result = db_query($sql, array(':tid' => $id));
 
while ($obj = $result->fetchObject()) {
  $array[] = array('nid' => $obj->entity_id, 'title' => $obj->name);
}
echo drupal_json_encode($array);

Well that’s it. There is one caveat when using jQuery. With a node-based form, the values in the second dropdown must already exist in the form or you will receive the following error when you try to save:

Illegal Choice Error

This error appears mostly when the field you are filling is a required field (but other times as well). So if a dropdown is empty and then filled with data, Drupal thinks something is wrong (for node-based only). At first I thought that the error was ridiculous, but I realized that the integrity of a Drupal form is vital. There are ways to get around it, but, in most cases, I just use Drupal’s Ajax instead of jQuery. Enjoy.

Be Sociable, Share!

Checkout My New Site - T-shirts For Geeks