Drupal How To: Customize Autocomplete Labels

January 30, 2019
Drupal Customize Autocomplete Labels blog image

Humans are pretty good at relating information. As a content editor, you might find yourself frequently exercising this skill, perhaps by associating products to a particular category or blog posts to topics. Drupal presents some well-designed user interfaces to assist. One of them is autocomplete, but you may need to customize it to maximize its usefulness.

What is autocomplete?

There are several autocomplete modules such as Search API Autocomplete, which provides autocomplete for a search index.

The one we want to focus on is the autocomplete that comes out-of-the-box in Drupal 8. When you’re in the Drupal admin area editing content, you might see a text input that looks like this:

Drupal 8 Autocomplete Feature screenshot

That’s an autocomplete, and more specifically it’s an autocomplete field widget that’s used on entity reference fields by default. It’s extremely useful. It’s a much better alternative to the classic select list when you’re dealing with large or unbounded datasets like a list of products or blog posts.

You don’t need to rely on memory when using an autocomplete; just type what you remember and the autocomplete will help fill in for you. Yay, technology!

The Problem

But, there’s a problem with the out-of-the-box autocomplete: the results show just the label of the referenced entity. The pattern looks like this:

Pattern: Entity Label

Using the example of relating blog posts to topics, you might type “An” for the topic and get an autocomplete list of “Announcements” and “Analytics.” For many data sets, that’s fine, like a list of states and provinces for a given country or a list of job titles and departments. But, there are common cases where this isn’t enough information.

Have you ever typed in an autocomplete and found two results with the same label? You have to trial and error to get the right one.

Or, maybe what you’re selecting is part of a hierarchy of content and you don’t know where it shows up. You have to go digging to find the right one, maybe in a new tab so you don’t lose your spot.

These are easy problems to fix!

Patterns for Autocomplete Labels

Let’s go through a few scenarios to show how you can customize autocomplete to improve the editor’s ability to quickly reference what they want.

Showing Hierarchy

Consider a scenario where you are autocompleting against is hierarchical data like a product catalog. Perhaps you have two product categories called “Seasonal,” one is in “Wall Calendars,” and the other is in “Desk Calendars.”

The default autocomplete list will look like “Seasonal,” “Seasonal,” and you won’t know which is which! But, showing the parents of the term in the label will make it clear:

Drupal 8 Autocomplete Default screenshot

Pattern: Parent Entity Label → Entity Label

Now, your autocomplete list looks like "Desk Calendars → Seasonal," and  "Wall Calendars → Seasonal."

Showing Published Status

Another scenario to consider is adding a piece of content to the site that shows relevant blog posts. The default labels don’t tell you if you’re selecting a post that is unpublished! Avoid this by adding that information in the label.

Drupal 8 Default Label Showing Unpublished screenshot

Pattern: Entity Label - Unpublished Indicator

Now, your autocomplete list will show you whether the content is viewable on the site. As shown in the example, the “Unpublished” next to the second post indicates it’s not viewable.

Showing Store or Domain Availability

If you’re using Drupal Commerce or the Domain module, you’ll have content that is only visible to a particular store or a particular domain. These problems are similar: you see your autocomplete list and you want to make sure if you’re creating content for a given store has references to content viewable to users on that store. Same goes for domains.

You can make this clear by adding the store or domain availability to the label.

Drupal 8 Autocomplete Default Feature Showing Store Screenshot

Pattern: Entity Label - Store Unavailability

In this example list we see that the “2019 Zebra Calendar” is unavailable in the Brand 1 store and the “2018 Zebra Calendar,” is unavailable in all stores because it is unpublished.

Showing Uniqueness

Sometimes the items in the autocomplete list are lengthy enough you don’t have room to add information to the label. However, you still need to differentiate between items with the same (or similar) label.

Recall the example of two “Seasonal” categories? An alternative to distinguishing them through showing hierarchy is to show the autogenerated ID.

Drupal 8 Autocomplete Feature Showing Uniqueness Screenshot

Pattern: Entity Label (Entity ID)

Here we can see that they are different by the entity ID. And, because every entity has an ID this pattern can apply to any type of entity. While it’s not ideal to have to remember which ID is which it is convenient to implement. And, since ID numbers are created sequentially, you can tell that lower ID numbers mean it was created before those with higher ID numbers.

Other Patterns

There are many other patterns worth considering. As long as the information you want to display in the autocomplete list belongs to the entities you’re referencing, it should be workable. Important things to consider are the width of the autocomplete list (long labels might get truncated) and whether you’re giving the editor enough information to work with.

Autocomplete is supposed to improve our content-entry experience. What is considered an improvement is relative to the information being referenced.

Customizing Autocomplete

If you’re not interested in seeing code, you can skip this section. Let’s look at what you’ll need to write to implement some of the patterns above.

On a high level, we’re working towards having a module with a set of files that looks like this:

Module with File Sets screenshot

Let’s break this down by file:

custom_autocomplete_labels.info.yml

This is a plain module info file. Nothing to note here. See the drupal.org documentation for details.

CustomAutocompleteLabelsServiceProvider.php

This file has one key line:

public function alter(ContainerBuilder $container) {
    $definition = $container->getDefinition('entity.autocomplete_matcher');
    $definition->setClass('Drupal\custom_autocomplete_labels\EntityAutocompleteMatcher');
  }

}

This tells Drupal to use our custom matcher service instead of the one in core. Scott Weston elaborates on how this overriding works in his post, Drupal How To: Override a Core Drupal 8 Service.

EntityAutocompleteMatcher

This is our matcher service that will customize the autocomplete labels. There is one public method that is called getMatches which returns the autocomplete list. Here is an example implementation:

public function getMatches($target_id, $selection_handler, $selection_settings, $string = '') {
  $matches = [];
  if (!isset($string)) {
    return $matches;
  }
  // Get the matches with different limits based on type of referenced entity.
  $handler = $this->selectionManager->getInstance([
    'target_type' => $target_id,
    'handler' => $selection_handler,
    'handler_settings' => $selection_settings,
  ]);
  $match_operator = !empty($selection_settings['match_operator']) ? $selection_settings['match_operator'] : 'CONTAINS';
  if ($target_id == 'taxonomy_term' || $target_id == 'commerce_product') {
    $entity_labels = $handler->getReferenceableEntities($string, $match_operator, 25);
  }
  else {
    $entity_labels = $handler->getReferenceableEntities($string, $match_operator, 10);
  }
  // Customize the labels used in autocomplete.
  foreach ($entity_labels as $values) {
    foreach ($values as $entity_id => $label) {
      if ($target_id == 'taxonomy_term') {
        $custom_label = $this->getLabelForTerm($entity_id, $target_id, $label);
      }
      elseif ($target_id == 'commerce_product') {
        $custom_label = $this->getLabelForProduct($entity_id, $target_id, $label);
      }
      else {
        $custom_label = $this->getLabel($entity_id, $target_id, $label);
      }
      // Create a sanitized key.
      $key = "$label ($entity_id)";
      $key = preg_replace('/\s\s+/', ' ', str_replace("\n", '', trim(Html::decodeEntities(strip_tags($key)))));
      $key = Tags::encode($key);
      $matches[] = ['value' => $key, 'label' => $custom_label];
    }
  }
  return $matches;
}

You’ll notice that we limit the number of items in the autocomplete list based on the referenced entity type. Consider returning more values based on how many results you think are necessary for the editor, however, keep an eye on performance.

As we iterate over $entity_labels we call different protected methods based on the type of entity. This allows us to apply different patterns. For example, for taxonomy terms we do:

protected function getLabelForTerm($entity_id, $entity_type_id, $label) {
  $term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
  $parents = $term_storage->loadAllParents($entity_id);
  $first = array_pop($parents);
  $label = $first->getName();
  foreach (array_reverse($parents) as $term) {
    $label = $label . ' → ' . $term->getName();
  }
  return $label;
}

This uses the “Showing Hierarchy” pattern we discussed earlier.

For products, we add different information. We use a combination of the show the “Showing Store or Domain Availability” and “Showing Published Status” patterns:

protected function getLabelForProduct($entity_id, $entity_type_id, $label) {
 $entity = \Drupal::entityTypeManager()->getStorage($entity_type_id)->load($entity_id);
 $available_store_ids = $entity->getStoreIds();
 $bundle_label = \Drupal::entityManager()->getBundleInfo('commerce_product')[$entity->bundle()]['label'];
 $all_stores = \Drupal::entityTypeManager()->getStorage('commerce_store')->loadMultiple();
 $label = $entity->label() . ' (' . $bundle_label . ')';
 if ($entity instanceof EntityPublishedInterface && !$entity->isPublished()) {
  $label = $label . ' - Unpublished';
 }
 elseif (count($available_store_ids) == 0) {
  $label = $label . ' - Unavailable in all stores';
 }
 else {
  $unavailable_stores = [];
  foreach ($all_stores as $store) {
   if (!in_array($store->id(), $available_store_ids)) {
    $unavailable_stores[] = $store->label();
   }
  }
  if (count($unavailable_stores)) {
   $label = $label . ' - Unavailable in ' . join($unavailable_stores);
  }
 }
 return $label;
}

Lastly, for all other types of entities, we are using the “Showing Published Status” and “Showing Uniqueness” patterns:

protected function getLabel($entity_id, $entity_type_id, $label) {
  $entity = \Drupal::entityTypeManager()->getStorage($entity_type_id)->load($entity_id);
  $entity = \Drupal::entityManager()->getTranslationFromContext($entity);
  $label = $label . ' (' . $entity_id . ')';
  if ($entity instanceof EntityPublishedInterface && !$entity->isPublished()) {
    $label = $label . ' - Unpublished';
  }
  return $label;
}

Summary

While customizing the autocomplete labels requires custom development now, we hope to see the “View output is not used for entityreference options,” issue resolved which will allow us to accomplish the same goal without custom coding. In fact, it would allow people other than developers, such as site builders, to help adjust and improve a site’s autocomplete labels.

Yet, this highlights one of the advantages of Drupal: most any piece of functionality, such as autocomplete, can be customized and improved to meet the use case for which it’s built. Consider how these patterns can be used to customize your Drupal site and what information your content editors would want to see when relating information with an autocomplete.

Also, consider when autocomplete isn’t the right user interface. If the user isn’t familiar with what the options are to choose from, it might be easier for them to see the list of items using a select list or radio buttons. If the selection is complex, a searchable select list like the Select 2 module might be a better fit. Take a look at the select2 documentation example for how it can offer a rich alternative to autocomplete. Or, use the Entity Browser module which gives the ability to go beyond text labels to being able to render the entity as part of the search.

If you’d like to see a working example of the code above, take a look at our drupal-custom-autocomplete-labels repository on BitBucket.