Drupal provides an AJAX API for working with Ajax. It offers predefined commands and an Ajax implementation for handling Ajax requests and responses. More details about the Ajax API can be found at https://www.drupal.org/docs/drupal-apis/ajax-api.

In this gist, we will see how we can implement Ajax form submission and provide a response using Ajax API.

<?php

namespace Drupal\finder\Form;

use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\finder\Service\ProgramFinder;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provide a search form for menu finder.
 */
class MenuSearchForm extends FormBase {

  /**
   * Self form id.
   */
  const FORM_ID = 'menu_search_form';

  /**
   * ProgramFinder service.
   *
   * @var \Drupal\finder\Service\ProgramFinder
   */
  protected ProgramFinder $programFinder;

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return static::FORM_ID;
  }

  /**
   * {@inheritdoc}
   */
  public function __construct(ProgramFinder $programFinder) {
    $this->programFinder = $programFinder;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    // Instantiates this form class.
    return new static(
      $container->get('program.finder')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    $form['keyword'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Keyword'),
      '#attributes' => [
        'placeholder' => $this->t('What are you interested in?'),
      ],
    ];

    $form['actions'] = [
      '#type' => 'button',
      '#value' => $this->t('Search'),
      '#attributes' => [
        'class' => [
          'btn',
          'btn-md',
          'btn-primary',
        ],
      ],
      '#ajax' => [
        'callback' => '::searchItems',
        'progress' => [
          'type' => 'throbber',
          'message' => NULL,
        ],
      ],
    ];

    return $form;
  }

  /**
   * @inheritDoc
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    // TODO: Implement submitForm() method.
  }

  /**
   * Ajax callback.
   *
   * @param array $form
   *   Form field array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Form state object.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   Ajax's response with command.
   */
  public function searchItems(array $form, FormStateInterface $form_state) {
    // Empty ajax response.
    $response = new AjaxResponse();

    // Get search results.
    $result = $this->programFinder->getResult($form_state->getValue('keyword'));

    // Prepare theme variables.
    $theme = [
      '#theme' => 'program_item',
      '#items' => isset($result['items']) ? $result['items'] : NULL,
      '#view_all' => isset($result['view_all']) ? $result['view_all'] : NULL,
    ];

    // Add ajax command.
    $response->addCommand(new HtmlCommand('#finder-results', $theme));

    return $response;
  }

}

In the example above, we bind an Ajax callback function called "searchItems" to the search button. This function is triggered when the search button is pressed. The searchItems callback processes the provided input and returns an Ajax response that includes a command to replace the HTML of the provided CSS selector.

Conditional form field with AJAX API

<?php

namespace Drupal\curator\Form;

use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Form\FormStateInterface;
use Drupal\curator\CuratorApiManager;
use Drupal\curator\CuratorManager;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provide entity form for curator entity.
 */
class CuratorForm extends EntityForm {

  /**
   * Entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManager
   */
  protected EntityTypeManager $entityTypeManager;

  /**
   * CuratorManager object.
   *
   * @var \Drupal\curator\CuratorManager
   */
  protected CuratorManager $curatorManager;

  /**
   * Api manager object.
   *
   * @var \Drupal\curator\CuratorApiManager
   */
  protected CuratorApiManager $curatorApiManager;

  /**
   * CuratorForm constructor.
   *
   * @param \Drupal\Core\Entity\EntityTypeManager $entityTypeManager
   *   Entity type manager service.
   * @param \Drupal\curator\CuratorManager $curatorManager
   *   Curator manager.
   * @param \Drupal\curator\CuratorApiManager $curatorApiManager
   *   Curator api manager.
   */
  public function __construct(EntityTypeManager $entityTypeManager,
                              CuratorManager $curatorManager,
                              CuratorApiManager $curatorApiManager) {
    $this->entityTypeManager = $entityTypeManager;
    $this->curatorManager = $curatorManager;
    $this->curatorApiManager = $curatorApiManager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('entity_type.manager'),
      $container->get('curator.manager'),
      $container->get('curator.api_manager')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function form(array $form, FormStateInterface $form_state) {
    $form = parent::form($form, $form_state);

    $curator = $this->entity;

    $form['label'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Label'),
      '#maxlength' => 255,
      '#default_value' => $curator->label(),
      '#description' => $this->t("Label for the curator feed."),
      '#required' => TRUE,
    ];
    $form['id'] = [
      '#type' => 'machine_name',
      '#default_value' => $curator->id(),
      '#machine_name' => [
        'exists' => [$this, 'exist'],
      ],
      '#disabled' => !$curator->isNew(),
    ];

    $form['enable'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Enable this curator instance'),
      '#default_value' => $curator->isEnabled(),
    ];

    $form['limit'] = [
      '#type' => 'number',
      '#title' => $this->t('Limit'),
      '#description' => $this->t("Number of post to load from api."),
      '#default_value' => $curator->limit(),
    ];

    $feeds = $this->curatorApiManager->getFeeds();
    $feeds_options = [];
    if (!empty($feeds)) {
      foreach ($feeds as $feed) {
        $feeds_options[$feed->id] = $feed->name;
      }
    }
    $form['feed'] = [
      '#type' => 'select',
      '#title' => $this->t('Choose Feed.'),
      '#options' => $feeds_options,
      '#disabled' => FALSE,
      '#required' => TRUE,
      '#default_value' => $curator->getFeedId() ? $curator->getFeedId() : '',
    ];

    $form['content_type'] = [
      '#type' => 'select',
      '#title' => $this->t('Select content type'),
      '#required' => TRUE,
      '#options' => $this->curatorManager->getContentType(),
      '#empty_option' => t('-- Select Content Type  --'),
      '#default_value' => $curator->getContentType(),
      '#limit_validation_errors' => [['content_type']],
      '#submit' => ['::submitContentType'],
      '#executes_submit_callback' => TRUE,
      '#ajax' => [
        'callback' => '::ajaxReplaceFieldsKeys',
        'wrapper' => 'field-key-settings',
        'method' => 'replace',
      ],
    ];

    $form['field_key_settings'] = [
      '#type' => 'container',
      '#prefix' => '<div id="field-key-settings">',
      '#suffix' => '</div>',
    ];

    if (!empty($curator->getContentType())) {

      $form['field_key_settings']['field_mapping_form'] = [
        '#type' => 'container',
        '#prefix' => '<div id="field-mapping-form">',
        '#suffix' => '</div>',
      ];

      $form['field_key_settings']['field_mapping_form'] = $this->curatorManager->getMappingElements(
        $form['field_key_settings']['field_mapping_form'],
        $curator->getSettings(),
        $curator->getMappingList(),
        $curator->getContentType()
      );

    }
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function save(array $form, FormStateInterface $form_state) {
    $curator = $this->entity;

    $feed = $form_state->getValue('feed');
    $settings = [];

    if ($feed) {
      $mappings = $curator->getMappingList();
      foreach ($mappings as $key => $type) {
        $settings[$feed][$key] = $form_state->getValue($key);
      }
    }

    $curator->set('setting', $settings);
    $status = $curator->save();

    if ($status) {
      \Drupal::messenger()
        ->addMessage($this->t('Saved the %label curator.', [
          '%label' => $curator->label(),
        ]));
    }
    else {
      \Drupal::messenger()
        ->addMessage($this->t('The %label curator was not saved.', [
          '%label' => $curator->label(),
        ]));
    }

    $form_state->setRedirect('entity.curator.collection');
  }

  /**
   * Helper function to check whether an curator configuration entity exists.
   *
   * @param int $id
   *   Entity id.
   */
  public function exist($id) {
    $entity = $this->entityTypeManager->getStorage('curator')->getQuery()
      ->condition('id', $id)
      ->count()
      ->execute();
    return (bool) $entity;
  }

  /**
   * Ajax replace field container.
   *
   * @param array $form
   *   Form field array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Form state object.
   */
  public function ajaxReplaceFields($form, FormStateInterface $form_state) {
    return $form['field_key_settings']['field_mapping_form'];
  }

  /**
   * Ajax replace field key container.
   *
   * @param array $form
   *   Form field array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Form state object.
   */
  public function ajaxReplaceFieldsKeys($form, FormStateInterface $form_state) {
    return $form['field_key_settings'];
  }

  /**
   * Handles submit call when content type is selected.
   *
   * @param array $form
   *   Form field array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Form state object.
   */
  public function submitContentType(array $form, FormStateInterface $form_state) {
    $this->entity = $this->buildEntity($form, $form_state);
    $form_state->setRebuild();
  }

}

In the example above, we have bind the function "ajaxReplaceFieldsKeys" to the content_type field. Additionally, we have limited the form validation to the field itself, triggering it only when the value is changed. 

When the value of the select field (content_type) is changed, and its Ajax callback, ajaxReplaceFieldsKeys, is invoked. This callback saves intermediate values of the form and rebuilds it.

Finally, our condition has the value of the content_type field to process it further.