How to create a custom autocomplete field and populate multiple fields with Ajax

Auto populating multiple form fields on Ajax is relatively easy in Drupal 8 and 9. Let's say that you have an autocomplete field and after the user selects an item from it you want to populate some other fields.

If your use case is simple enough and all you need is a basic autocomplete field then you should use the entity_autocomplete form element. This is a generic Form API element that works with entities in all forms (regular and entity forms). Here's an example of the simplest autocomplete field that matches node titles from Articles:

$form['article'] = [
  '#type' => 'entity_autocomplete',
  '#title' => $this->t('Article'),
  '#target_type' => 'node',
  '#selection_settings' => [
    'target_bundles' => ['article'],
  ],
];

Now that we know how to add a simple entity autocomplete form field let's see how to populate other fields based on the result of this autocomplete field.

Our example form will have the autocomplete field named Article and two textual fields, Authored on and Authored by. So here's the screenshot of the form:

Image

And here's the code used to build the form in Drupal:

use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

class SomeForm extends FormBase {

  ...

  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['article'] = [
      '#type' => 'entity_autocomplete',
      '#title' => $this->t('Article'),
      '#target_type' => 'node',
      '#selection_settings' => [
        'target_bundles' => ['article'],
      ],
      '#ajax' => [
        'callback' => '::populateFields',
        'event' => 'autocompleteclose',
      ],
    ];

    $form['authored_on'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Authored on'),
    ];

    $form['authored_by'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Authored by'),
    ];

    return $form;
  }

  public function populateFields(array $form, FormStateInterface $form_state) {
    $response = new AjaxResponse();

    $article_id = $form_state->getValue('article');
    if (!empty($article_id)) {
      $article = \Drupal::entityTypeManager()->getStorage('node')->load($article_id);
      $response->addCommand(new InvokeCommand('#edit-authored-on', 'val', [$article->getCreatedTime()]));
      $response->addCommand(new InvokeCommand('#edit-authored-by', 'val', [$article->getOwner()->getDisplayName()]));
    }

    return $response;
  }

  ...

}

As you can see we added the #ajax property to the Article field. We specified the callback method name and event on which we want to populate fields.

Each time the autocomplete field is used the populateFields() method will be called, and that method will get the article ID, then load the article and finally add the Authored on and Authored by values to appropriate fields by using the InvokeCommand.

The InvokeCommand is an AJAX command for invoking an arbitrary jQuery method. In this case, we are using the val() method to set the field values.

Custom autocomplete field

Now let's see how to create a custom autocomplete field, but first, why do you even need a custom autocomplete field when there is one already in the core?

You need it if you want to have a customized query. Maybe you want to add some query conditions or sort results differently, or do something else.

Create a routing file if you already don't have it and add the autocomplete route:

MY_MODULE.routing.yml

MY_MODULE.node_autocomplete:
  path: '/node-autocomplete'
  defaults:
    _controller: '\Drupal\MY_MODULE\Controller\NodeAutocompleteController::autocomplete'
    _format: json
  requirements:
    _access: 'TRUE'

We want our autocomplete field to be available to all users (authenticated and anonymous) hence we are setting the access to TRUE, which means that access to this route is granted in all circumstances.

Now create the controller and implement the autocomplete method (you can call it any way you want):

src/Controller/NodeAutocompleteController.php

<?php

namespace Drupal\MY_MODULE\Controller;

use Drupal\Component\Utility\Xss;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\Element\EntityAutocomplete;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;

class NodeAutocompleteController extends ControllerBase {

  public function autocomplete(Request $request) {
    $results = [];

    $keyword = Xss::filter($request->query->get('q'));
    if (empty($keyword)) {
      return new JsonResponse($results);
    }

    $query = \Drupal::entityTypeManager()->getStorage('node')->getQuery()
      ->condition('title', $keyword, 'CONTAINS')
      ->condition('uid', 1, '<>')
      ->sort('created', 'DESC')
      ->range(0, 8);

    $ids = $query->execute();
    $items = $ids ? \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($ids) : [];

    foreach ($items as $item) {
      $label = [];
      $label[] = $item->getTitle();

      $results[] = [
        'value' => EntityAutocomplete::getEntityLabels([$item]),
        'label' => implode(', ', $label) . ' (' . $item->id() . ')',
      ];
    }

    return new JsonResponse($results);
  }

}

And finally, we can create a form with our new custom autocomplete field. In this case, we are using the textual field type and adding the #autocomplete_route_name property. The value for this property is the name of the autocomplete route.

src/Form/SomeForm.php

use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

class SomeForm extends FormBase {

  ...

  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['article'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Article'),
      '#autocomplete_route_name' => 'MY_MODULE.node_autocomplete',
      '#ajax' => [
        'callback' => '::populateFields',
        'event' => 'autocompleteclose',
      ],
    ];

    $form['authored_on'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Authored on'),
    ];

    $form['authored_by'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Authored by'),
    ];

    return $form;
  }

  public function populateFields(array $form, FormStateInterface $form_state) {
    $response = new AjaxResponse();

    $article_id = EntityAutocomplete::extractEntityIdFromAutocompleteInput($form_state->getValue('article'));
    if (!empty($article_id)) {
      $article = \Drupal::entityTypeManager()->getStorage('node')->load($article_id);
      $response->addCommand(new InvokeCommand('#edit-authored-on', 'val', [$article->getCreatedTime()]));
      $response->addCommand(new InvokeCommand('#edit-authored-by', 'val', [$article->getOwner()->getDisplayName()]));
    }

    return $response;
  }

  ...

}

As you can see the code is a bit different but essentially it does the same thing.

NOTE: For the sake of brevity, I didn't use dependency injection here to inject the Entity type manager but you should do it in your code.

… and that’s it! Now go on and create some autocomplete fields. And if you need help with anything let me know.

About the Author

Goran Nikolovski is an experienced web and AI developer skilled in Drupal, React, and React Native. He founded this website and enjoys sharing his knowledge.