AJAX dependent select in Drupal 8 and 9

In order to avoid frustrations, having a code snippet ready to be copy & pasted comes in handy in many situations when you are working with AJAX and Drupal 8 or 9. This is such an article, so don't expect too much theory -- just some code snippets.

By the way, I have a lot more Drupal 8 and 9 code snippets ready to be used as examples or reused in your own projects.

Let's see what are we trying to accomplish in this article.

We have a simple form with two select boxes, Country and City. When you change the Country select box the City field also has to change accordingly.

If a picture is worth a thousand words then the following gif animation is all you need to understand the problem we are trying to solve.

AJAX dependent select in Drupal 8 and 9

First, let's create the route for our Ajax form.

MY_MODULE.ajax_form:
  path: '/ajax-form'
  defaults:
    _form: '\Drupal\MY_MODULE\Form\AjaxForm'
    _title: 'Ajax form'
  requirements:
    _permission: 'access content'

Nothing fancy here just a simple route that points to the form.

Now let's create the Ajax form itself.

<?php

namespace Drupal\MY_MODULE\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

class AjaxForm extends FormBase {

  public function getFormId() {
    return 'MY_MODULE_ajax_form';
  }

  public function buildForm(array $form, FormStateInterface $form_state) {
    $triggering_element = $form_state->getTriggeringElement();

    $form['country'] = [
      '#type' => 'select',
      '#title' => $this->t('Country'),
      '#options' => [
        'serbia' => $this->t('Serbia'),
        'usa' => $this->t('USA'),
        'italy' => $this->t('Italy'),
        'france' => $this->t('France'),
        'germany' => $this->t('Germany'),
      ],
      '#ajax' => [
        'callback' => [$this, 'reloadCity'],
        'event' => 'change',
        'wrapper' => 'city-field-wrapper',
      ],
    ];

    $form['city'] = [
      '#type' => 'select',
      '#title' => $this->t('City'),
      '#options' => [
        'belgrade' => $this->t('Belgrade'),
        'washington' => $this->t('Washington'),
        'rome' => $this->t('Rome'),
        'paris' => $this->t('Paris'),
        'berlin' => $this->t('Berlin'),
      ],
      '#prefix' => '<div id="city-field-wrapper">',
      '#suffix' => '</div>',
      '#value' => empty($triggering_element) ? 'belgrade' : $this->getCityForCountry($triggering_element['#value']),
    ];

    return $form;
  }

  public function reloadCity(array $form, FormStateInterface $form_state) {
    return $form['city'];
  }

  protected function getCityForCountry($country) {
    $map = [
      'serbia' => 'belgrade',
      'usa' => 'washington',
      'italy' => 'rome',
      'france' => 'paris',
      'germany' => 'berlin',
    ];

    return $map[$country] ?? NULL;
  }

  public function submitForm(array &$form, FormStateInterface $form_state) {
    //
  }

}

As you can see the Country select box is Ajaxified. On changing the value, the City select box is updated.

The most interesting line is this:

empty($triggering_element) ? 'belgrade' : $this->getCityForCountry($triggering_element['#value']),

If we don't have a form element that triggered submission then we set 'belgrade' as the default value for the City field. Otherwise, we are setting the appropriate City based on the selected Country value.

About the Author

Goran Nikolovski is a creator, speaker, open-source contributor, web developer specialized in Drupal and DevOps enthusiast. He is the founder of this website and he enjoys sharing his knowledge and experiences.