Dependent Autocomplete With Entity Reference in Drupal

Dependent Autocomplete with Entity Reference

There are times when you prompt your user with two autocomplete input boxes where one depends on the information entered into the first box. For example, if you select "cars" in the first box, then all the autocomplete value suggestions would pertain to "cars" in the second box. The values that you load in the second box depends on the selection of the first box.


Here are the steps to create Dynamic autocomplete list in the form (Dependent Autocomplete).

1. Add a vocabulary with child terms as shown in screenshot.

Dependent Autocomplete with Entity Reference

2. Create an entity reference view with 3 blocks for created vocabulary Parent term, Child term, Child term - All.

  • Parent Term - All (It will show all parent terms.)
Dependent Autocomplete with Entity Reference
  • Child Term - All (It will show all child terms.)
Dependent Autocomplete with Entity Reference
  • Child Term (It will show child term of selected parent term with contextual filter.)
Dependent Autocomplete with Entity Reference

3. In your content type, create two fields for parent term (Reference Parent Term - All view) and child term (Child Term - All view) as entity referenced and widget type "autocomplete".

4. Now, create a new module and javascript file and paste the following code in your module file and make changes as per your requirements.

  • Module code
/**
               * @file
               * Code for Dependent Autocomplete.
               */

              /**
               * Implements hook_menu().
               */
              function mymodule_autocomplete_menu() {

                $items['mymodule/autocomplete/single/%/%/%'] = array(
                  'title' => 'Entity Reference Autocomplete',
                  'page callback' => 'mymodule_autocomplete_autocomplete_callback',
                  'page arguments' => array(2, 3, 4, 5),
                  'access callback' => 'mymodule_autocomplete_access_callback',
                  'access arguments' => array(2, 3, 4, 5),
                  'type' => MENU_CALLBACK,
                );

                $items['set-manufacture-sesn'] = array(
                  'title' => 'Set session',
                  'page callback' => 'mymodule_autocomplete_set_session_make',
                  'access arguments' => array('access content'),
                );

                return $items;
              }

              /**
               * Implements hook_js_alter().
               */
              function mymodule_autocomplete_js_alter(&$javascript) {
                $javascript['misc/autocomplete.js']['data'] = drupal_get_path('module', 'mymodule_autocomplete') . '/autocomplete.js';
              }


              /**
               * function for set session of selected parent term.
               */
              function mymodule_autocomplete_set_session_make() {

                $requested_parent = $_REQUEST['parent_term'];
                $explaode_val_temp = explode("(", $requested_parent);

                $parent_tid = str_replace(")", "", $explaode_val_temp[1]);

                $_SESSION['PARENT_TID'] = $parent_tid;

              }


              /**
               * Implements hook_form_FORM_ID_alter().
               */
              function mymodule_autocomplete_form_new_content_type_node_form_alter(&$form, &$form_state, $form_id) {

                unset($_SESSION['PARENT_TID']);

                if (isset($form_state['node']) && !empty($form_state['node']->field_equipment_manufacture[LANGUAGE_NONE]['0']['target_id'])) {
                  $_SESSION['PARENT_TID'] = $form_state['node']->field_equipment_manufacture[LANGUAGE_NONE]['0']['target_id'];
                }

                $form['field_equipment_model'][LANGUAGE_NONE]['0']['target_id']['#autocomplete_path'] = 'mymodule/autocomplete/single/field_equipment_model/node/new_content_type/NULL';

                $form['#validate'][] = 'mymodule_autocomplete_equipmentnode_form_validate';

                // Attach js to form
                $form['#attached']['js'] = array(
                  drupal_get_path('module', 'mymodule_autocomplete') . '/mymodule_autocomplete.js',
                );
              }

              /**
               * Validates the category attributes form.
               */
              function mymodule_autocomplete_equipmentnode_form_validate($form, &$form_state) {
                
                if (isset($form_state['values']['field_equipment_manufacturer'][LANGUAGE_NONE]['0']['target_id']) && !empty($form_state['values']['field_equipment_manufacturer'][LANGUAGE_NONE]['0']['target_id'])) {
                  $_SESSION['PARENT_TID'] = $form_state['values']['field_equipment_manufacturer'][LANGUAGE_NONE]['0']['target_id'];
                }
                
              }

              /**
               * Menu Access callback for the autocomplete widget.
               *
               * @param $type
               *   The widget type (i.e. 'single' or 'tags').
               * @param $field_name
               *   The name of the entity-reference field.
               * @param $entity_type
               *   The entity type.
               * @param $bundle_name
               *   The bundle name.
               * @return
               *   True if user can access this menu item.
               */
              function mymodule_autocomplete_access_callback($type, $field_name, $entity_type, $bundle_name) {
                $field = field_info_field($field_name);
                $instance = field_info_instance($entity_type, $field_name, $bundle_name);

                if (!$field || !$instance || $field['type'] != 'entityreference' || !field_access('edit', $field, $entity_type)) {
                  return FALSE;
                }
                return TRUE;
              }

              /**
               * Menu callback: autocomplete the label of an entity.
               *
               * @param $type
               *   The widget type (i.e. 'single' or 'tags').
               * @param $field_name
               *   The name of the entity-reference field.
               * @param $entity_type
               *   The entity type.
               * @param $bundle_name
               *   The bundle name.
               * @param $entity_id
               *   Optional; The entity ID the entity-reference field is attached to.
               *   Defaults to ''.
               * @param $string
               *   The label of the entity to query by.
               */
              function mymodule_autocomplete_autocomplete_callback($type, $field_name, $entity_type, $bundle_name, $entity_id = '', $string = '') {
                // If the request has a '/' in the search text, then the menu system will have
                // split it into multiple arguments and $string will only be a partial. We want
                //  to make sure we recover the intended $string.
                $args = func_get_args();
                // Shift off the $type, $field_name, $entity_type, $bundle_name, and $entity_id args.
                array_shift($args);
                array_shift($args);
                array_shift($args);
                array_shift($args);
                array_shift($args);
                $string = implode('/', $args);

                $field = field_info_field($field_name);
                $instance = field_info_instance($entity_type, $field_name, $bundle_name);
                

                return mymodule_autocomplete_callback_get_matches($type, $field, $instance, $entity_type, $entity_id, $string);
              }

              /**
               * Return JSON based on given field, instance and string.
               *
               * This function can be used by other modules that wish to pass a mocked
               * definition of the field on instance.
               *
               * @param $type
               *   The widget type (i.e. 'single' or 'tags').
               * @param $field
               *   The field array definition.
               * @param $instance
               *   The instance array definition.
               * @param $entity_type
               *   The entity type.
               * @param $entity_id
               *   Optional; The entity ID the entity-reference field is attached to.
               *   Defaults to ''.
               * @param $string
               *   The label of the entity to query by.
               */
              function mymodule_autocomplete_callback_get_matches($type, $field, $instance, $entity_type, $entity_id = '', $string = '') {
                $matches = array();

                $entity = NULL;
                if ($entity_id !== 'NULL') {
                  $entity = entity_load_single($entity_type, $entity_id);
                  $has_view_access = (entity_access('view', $entity_type, $entity) !== FALSE);
                  $has_update_access = (entity_access('update', $entity_type, $entity) !== FALSE);
                  if (!$entity || !($has_view_access || $has_update_access)) {
                    return MENU_ACCESS_DENIED;
                  }
                }

                $handler = mymodule_get_selection_handler($field, $instance, $entity_type, $entity);

                if (isset($_SESSION['PARENT_TID'])) {
                  $handler->field['settings']['handler_settings']['view']['display_name'] = 'entityreference_2';
                  $handler->field['settings']['handler_settings']['view']['args'][0] = $_SESSION['PARENT_TID'];
                }

                if ($type == 'tags') {
                  // The user enters a comma-separated list of tags. We only autocomplete the last tag.
                  $tags_typed = drupal_explode_tags($string);
                  $tag_last = drupal_strtolower(array_pop($tags_typed));
                  if (!empty($tag_last)) {
                    $prefix = count($tags_typed) ? implode(', ', $tags_typed) . ', ' : '';
                  }
                }
                else {
                  // The user enters a single tag.
                  $prefix = '';
                  $tag_last = $string;
                }

                if (isset($tag_last)) {
                  // Get an array of matching entities.
                  $entity_labels = $handler->getReferencableEntities($tag_last, $instance['widget']['settings']['match_operator'], 10);

                  // Loop through the products and convert them into autocomplete output.
                  foreach ($entity_labels as $values) {
                    foreach ($values as $entity_id => $label) {
                      $key = "$label ($entity_id)";
                      // Strip things like starting/trailing white spaces, line breaks and tags.
                      $key = preg_replace('/\s\s+/', ' ', str_replace("\n", '', trim(decode_entities(strip_tags($key)))));
                      // Names containing commas or quotes must be wrapped in quotes.
                      if (strpos($key, ',') !== FALSE || strpos($key, '"') !== FALSE) {
                        $key = '"' . str_replace('"', '""', $key) . '"';
                      }
                      $matches[$prefix . $key] = '' . $label . '';
                    }
                  }
                }

                drupal_json_output($matches);
              }

              /**
               * Get the selection handler for a given entityreference field.
               */
              function mymodule_get_selection_handler($field, $instance = NULL, $entity_type = NULL, $entity = NULL) {
                ctools_include('plugins');
                $handler = $field['settings']['handler'];
                $class = ctools_plugin_load_class('entityreference', 'selection', $handler, 'class');

                if (class_exists($class)) {
                  return call_user_func(array($class, 'getInstance'), $field, $instance, $entity_type, $entity);
                }
                else {
                  return EntityReference_SelectionHandler_Broken::getInstance($field, $instance, $entity_type, $entity);
                }
              }
  • Javascript Code
(function ($) {
              Drupal.behaviors.mymoduleAutocomplete = {
                attach: function (context) {
                  $("#edit-field-equipment-manufacture-und-0-target-id", context).bind('autocompleteSelect', function(event, node) {

                    $eref = $('#edit-field-equipment-manufacture-und-0-target-id', context);

                    var key = $(node).data('autocompleteValue');
                    var val = $eref.val();
                    
                    $.ajax({
                      method: "POST",
                      url: Drupal.settings.basePath + "set-manufacture-sesn",
                      data: { parent_term : val},
                      dataType: 'json'
                    })
                    .done(function( result ) {
                      if (result.status == 200) {
                      }
                    });
                  });
                }
              };
            })(jQuery);

5. Overwrite autocomplete.js behaviour in your custom module.

  • autocomplete.js Code
(function ($) {
              /**
               * Attaches the autocomplete behavior to all required fields.
               */
              Drupal.behaviors.autocomplete = {
                attach: function (context, settings) {
                  var acdb = [];
                  $('input.autocomplete', context).once('autocomplete', function () {
                    var uri = this.value;
                    if (!acdb[uri]) {
                      acdb[uri] = new Drupal.ACDB(uri);
                    }
                    var $input = $('#' + this.id.substr(0, this.id.length - 13))
                      .attr('autocomplete', 'OFF')
                      .attr('aria-autocomplete', 'list');
                    $($input[0].form).submit(Drupal.autocompleteSubmit);
                    $input.parent()
                      .attr('role', 'application')
                      .append($('')
                        .attr('id', $input.attr('id') + '-autocomplete-aria-live')
                      );
                    new Drupal.jsAC($input, acdb[uri]);
                  });
                }
              };

              /**
               * Prevents the form from submitting if the suggestions popup is open
               * and closes the suggestions popup when doing so.
               */
              Drupal.autocompleteSubmit = function () {
                return $('#autocomplete').each(function () {
                  this.owner.hidePopup();
                }).length == 0;
              };

              /**
               * An AutoComplete object.
               */
              Drupal.jsAC = function ($input, db) {
                var ac = this;
                this.input = $input[0];
                this.ariaLive = $('#' + this.input.id + '-autocomplete-aria-live');
                this.db = db;

                $input
                  .keydown(function (event) { return ac.onkeydown(this, event); })
                  .keyup(function (event) { ac.onkeyup(this, event); })
                  .blur(function () { ac.hidePopup(); ac.db.cancel(); });

                };

              /**
               * Handler for the "keydown" event.
               */
              Drupal.jsAC.prototype.onkeydown = function (input, e) {
                if (!e) {
                  e = window.event;
                }
                switch (e.keyCode) {
                  case 40: // down arrow.
                    this.selectDown();
                    return false;
                  case 38: // up arrow.
                    this.selectUp();
                    return false;
                  default: // All other keys.
                    return true;
                }
              };

              /**
               * Handler for the "keyup" event.
               */
              Drupal.jsAC.prototype.onkeyup = function (input, e) {
                if (!e) {
                  e = window.event;
                }
                switch (e.keyCode) {
                  case 16: // Shift.
                  case 17: // Ctrl.
                  case 18: // Alt.
                  case 20: // Caps lock.
                  case 33: // Page up.
                  case 34: // Page down.
                  case 35: // End.
                  case 36: // Home.
                  case 37: // Left arrow.
                  case 38: // Up arrow.
                  case 39: // Right arrow.
                  case 40: // Down arrow.
                    return true;

                  case 9:  // Tab.
                  case 13: // Enter.
                  case 27: // Esc.
                    this.hidePopup(e.keyCode);
                    return true;

                  default: // All other keys.
                    if (input.value.length > 0 && !input.readOnly) {
                      this.populatePopup();
                    }
                    else {
                      this.hidePopup(e.keyCode);
                    }
                    return true;
                }
              };

              /**
               * Puts the currently highlighted suggestion into the autocomplete field.
               */
              Drupal.jsAC.prototype.select = function (node) {
                this.input.value = $(node).data('autocompleteValue');
                $(this.input).trigger('autocompleteSelect', [node]);
              };

              /**
               * Highlights the next suggestion.
               */
              Drupal.jsAC.prototype.selectDown = function () {
                if (this.selected && this.selected.nextSibling) {
                  this.highlight(this.selected.nextSibling);
                }
                else if (this.popup) {
                  var lis = $('li', this.popup);
                  if (lis.length > 0) {
                    this.highlight(lis.get(0));
                  }
                }
              };

              /**
               * Highlights the previous suggestion.
               */
              Drupal.jsAC.prototype.selectUp = function () {
                if (this.selected && this.selected.previousSibling) {
                  this.highlight(this.selected.previousSibling);
                }
              };

              /**
               * Highlights a suggestion.
               */
              Drupal.jsAC.prototype.highlight = function (node) {
                if (this.selected) {
                  $(this.selected).removeClass('selected');
                }
                $(node).addClass('selected');
                this.selected = node;
                $(this.ariaLive).html($(this.selected).html());
              };

              /**
               * Unhighlights a suggestion.
               */
              Drupal.jsAC.prototype.unhighlight = function (node) {
                $(node).removeClass('selected');
                this.selected = false;
                $(this.ariaLive).empty();
              };

              /**
               * Hides the autocomplete suggestions.
               */
              Drupal.jsAC.prototype.hidePopup = function (keycode) {
                // Select item if the right key or mousebutton was pressed.
                if (this.selected && ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode)) {
                  this.select(this.selected);
                }
                // Hide popup.
                var popup = this.popup;
                if (popup) {
                  this.popup = null;
                  $(popup).fadeOut('fast', function () { $(popup).remove(); });
                }
                this.selected = false;
                $(this.ariaLive).empty();
              };

              /**
               * Positions the suggestions popup and starts a search.
               */
              Drupal.jsAC.prototype.populatePopup = function () {
                var $input = $(this.input);
                var position = $input.position();
                // Show popup.
                if (this.popup) {
                  $(this.popup).remove();
                }
                this.selected = false;
                this.popup = $('')[0];
                this.popup.owner = this;
                $(this.popup).css({
                  top: parseInt(position.top + this.input.offsetHeight, 10) + 'px',
                  left: parseInt(position.left, 10) + 'px',
                  width: $input.innerWidth() + 'px',
                  display: 'none'
                });
                $input.before(this.popup);

                // Do search.
                this.db.owner = this;
                this.db.search(this.input.value);
              };

              /**
               * Fills the suggestion popup with any matches received.
               */
              Drupal.jsAC.prototype.found = function (matches) {
                // If no value in the textfield, do not show the popup.
                if (!this.input.value.length) {
                  return false;
                }

                // Prepare matches.
                var ul = $('');
                var ac = this;
                for (key in matches) {
                  $('')
                    .html($('').html(matches[key]))
                    .mousedown(function () { ac.hidePopup(this); })
                    .mouseover(function () { ac.highlight(this); })
                    .mouseout(function () { ac.unhighlight(this); })
                    .data('autocompleteValue', key)
                    .appendTo(ul);
                }

                // Show popup with matches, if any.
                if (this.popup) {
                  if (ul.children().length) {
                    $(this.popup).empty().append(ul).show();
                    $(this.ariaLive).html(Drupal.t('Autocomplete popup'));
                  }
                  else {
                    $(this.popup).css({ visibility: 'hidden' });
                    this.hidePopup();
                  }
                }
              };

              Drupal.jsAC.prototype.setStatus = function (status) {
                switch (status) {
                  case 'begin':
                    $(this.input).addClass('throbbing');
                    $(this.ariaLive).html(Drupal.t('Searching for matches...'));
                    break;
                  case 'cancel':
                  case 'error':
                  case 'found':
                    $(this.input).removeClass('throbbing');
                    break;
                }
              };

              /**
               * An AutoComplete DataBase object.
               */
              Drupal.ACDB = function (uri) {
                this.uri = uri;
                this.delay = 300;
                this.cache = {};
              };

              /**
               * Performs a cached and delayed search.
               */
              Drupal.ACDB.prototype.search = function (searchString) {
                var db = this;
                this.searchString = searchString;

                // See if this string needs to be searched for anyway. The pattern ../ is
                // stripped since it may be misinterpreted by the browser.
                searchString = searchString.replace(/^\s+|\.{2,}\/|\s+$/g, '');
                // Skip empty search strings, or search strings ending with a comma, since
                // that is the separator between search terms.
                if (searchString.length <= 0 ||
                  searchString.charAt(searchString.length - 1) == ',') {
                  return;
                }

                // See if this key has been searched for before.
                if (this.cache[searchString]) {
                  return this.owner.found(this.cache[searchString]);
                }

                // Initiate delayed search.
                if (this.timer) {
                  clearTimeout(this.timer);
                }
                this.timer = setTimeout(function () {
                  db.owner.setStatus('begin');

                  // Ajax GET request for autocompletion. We use Drupal.encodePath instead of
                  // encodeURIComponent to allow autocomplete search terms to contain slashes.
                  $.ajax({
                    type: 'GET',
                    url: db.uri + '/' + Drupal.encodePath(searchString),
                    dataType: 'json',
                    success: function (matches) {
                      if (typeof matches.status == 'undefined' || matches.status != 0) {
                        //db.cache[searchString] = matches;
                        // Verify if these are still the matches the user wants to see.
                        if (db.searchString == searchString) {
                          db.owner.found(matches);
                        }
                        db.owner.setStatus('found');
                      }
                    },
                    error: function (xmlhttp) {
                      alert(Drupal.ajaxError(xmlhttp, db.uri));
                    }
                  });
                }, this.delay);
              };

              /**
               * Cancels the current autocomplete request.
               */
              Drupal.ACDB.prototype.cancel = function () {
                if (this.owner) this.owner.setStatus('cancel');
                if (this.timer) clearTimeout(this.timer);
                this.searchString = '';
              };
            })(jQuery);

 Here is the final output. This is how dependent autocomplete fields work.

Dependent Autocomplete with Entity Reference