How to create a custom Autocomplete using the Drupal 8 Form API
In this article, I will not explain how to customize/alter an Autocomplete Field Widget — which should only be used on Form using the Drupal Admin UI.
Here I will try to expose you a step-by-step guide which explains how you can create a custom Autocomplete field using the Drupal 8 Form API Core feature — An autocomplete that you could use in your own Drupal frontend applications.
If you are looking for resources which explains how to implement Views to alter an Autocomplete Field, please refer to this excellent guide.
In the other hand, if you are looking for resources which explains how to create a custom Drupal Entity Field Widget Autocomplete that fetch data outside of Drupal (eg. from an API) but still store and display final data into Drupal Entities, please refer to my previous blog post.
Truth can only be found in one place: the code
- Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship
Step 1- The autocomplete form element
The other day, I was asked to create a custom Autocomplete for a project which uses a custom Form build using the Drupal 8 Form API.
At first sight, you may be interested to use the #entity_autocomplete
field type - it seems exactly what you need. Unfortunately, this is not. Indeed, the #entity_autocomplete
doesn't allow you any customisation.
So, you will need the old folk’s #textfield
and his cousin attribute #autocomplete_route_name
<?php
namespace Drupal\my_module\Form;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Entity\Element\EntityAutocomplete;
/**
* Form to handle article autocomplete.
*/
class ArticleAutocompleteForm extends FormBase {
/**
* The node storage.
*
* @var \Drupal\node\NodeStorage
*/
protected $nodeStorage;
/**
* {@inheritdoc}
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->nodeStorage = $entity_type_manager->getStorage('node');
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['article'] = [
'#type' => 'textfield',
'#title' => $this->t('My Autocomplete'),
'#autocomplete_route_name' => 'my_module.autocomplete.articles',
];
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Extracts the entity ID from the autocompletion result.
$article_id = EntityAutocomplete::extractEntityIdFromAutocompleteInput($form_state->getValue('article'));
}
}
The #autocomplete_route_name
attribute will allow you to define a route to handle the autocomplete business logic (data you will return given the user input).
You also may add the #autocomplete_route_parameters
attribute, this one gives you the possibility to send a fixed unalterable parameter to your #autocomplete_route_name
, you may use it to fix the number of results to return.
Step 2 — Define autocomplete route
Now you know how to create the autocomplete form, but you will need a route to manage the logic which will fetch data & return them.
How? By simply adding the reference to the route — where data will get retrieved from — to your my_module.routing.yml file:
my_module.autocomplete.articles:
path: '/admin/my_module/autocomplete/articles'
defaults:
_controller: '\Drupal\my_module\Controller\ArticleAutoCompleteController::handleAutocomplete'
_format: json
requirements:
Be careful to use the same route name (here my_module.autocomplete.articles
) in your previous #autocomplete_route_name
.
Also, be sure to change permission according to your own needs.
Step 3 — Add Controller and return JSON response
Now having a routing & a form, you have to define your custom controller, with the handleAutocomplete
method.
Well, it's precisely this method that makes sure that the proper data gets collected and properly formatted once requested by Drupal.
Let’s dig deeper and see how we can precisely deal with specific JSON response for our textfield
element.
- Setup an ArticleAutoCompleteController class file under my_module/src/Controller/ArticleAutoCompleteController.php;
- Then, extend the
ControllerBase
class and setup your handle method (in our case::handleAutocomplete
seemy_module.routing.yml
);
<?phpnamespace Drupal\my_module\Controller;use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Entity\Element\EntityAutocomplete;/**
* Defines a route controller for watches autocomplete form elements.
*/
class ArticleAutoCompleteController extends ControllerBase {/**
* The node storage.
*
* @var \Drupal\node\NodeStorage
*/
protected $nodeStorage;/**
* {@inheritdoc}
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->nodeStroage = $entity_type_manager->getStorage('node');
}/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
// Instantiates this form class.
return new static(
$container->get('entity_type.manager')
);
}/**
* Handler for autocomplete request.
*/
public function handleAutocomplete(Request $request) {
$results = [];
$input = $request->query->get('q');// Get the typed string from the URL, if it exists.
if (!$input) {
return new JsonResponse($results);
}$input = Xss::filter($input);$query = $this->nodeStroage->getQuery()
->condition('type', 'article')
->condition('title', $input, 'CONTAINS')
->groupBy('nid')
->sort('created', 'DESC')
->range(0, 10);$ids = $query->execute();
$nodes = $ids ? $this->nodeStroage->loadMultiple($ids) : [];foreach ($nodes as $node) {
switch ($node->isPublished()) {
case TRUE:
$availability = '✅';
break;case FALSE:
default:
$availability = '🚫';
break;
}$label = [
$node->getTitle(),
'<small>(' . $node->id() . ')</small>',
$availability,
];$results[] = [
'value' => EntityAutocomplete::getEntityLabels([$node]),
'label' => implode(' ', $label),
];
}return new JsonResponse($results);
}
}
That’s pretty much all the hocus-pocus that you need to have an autocomplete based on a textfield of Drupal 8.
Sources
This article has been first write for the Blog of Antistatique — Web Agency in Lausanne, Switzerland. A place where I work as Full Stack Web Developer.
Feel free to read it here or check it out there: https://antistatique.net/en/node/567
For the most curious of you, here are some sources of additional information that inspired the creation of this article.
Purushotam Rai (24 November, 2016). Implementing #autocomplete in Drupal 8 with Custom Callbacks
See on https://www.qed42.com/blog/autocom...
Stijn Berkers (28 February, 2018). How to produce a custom auto-complete field in drupal 8
See on https://lucius.digital/en/blog/drupal...
Make-me-alive (4 May, 2014). Adding autocomplete for text-field
See on https://drupal.stackexchange.com/.../..
Stijn Berkers (18 July, 2018). How to add autocomplete to text fields in drupal 8: defining a custom route
See on https://www.optasy.com/blog/how-add-autocom...