AJAX Forms

Last updated on
30 October 2023

This documentation needs review. See "Help improve this page" in the sidebar.

Overview

Adding AJAX callback events to form fields allows to dynamically update fields and other markup, while users interact with the forms.

More general information about Drupal and Ajax can be found at Drupal Ajax API Guide and Drupal's Ajax API documentation (at api.drupal.org).

Some working examples can be found in the Examples for Developers modules, specifically in the AJAX Example submodule, see its Form folder.

AJAX callback functions on forms can be used to:

  • Update existing or add new form fields,
  • update values in form fields
  • execute a variety of predefined AJAX commands or
  • run custom JavaScript code

If you simply want to create conditional form fields that can be shown, hidden, enabled or disabled based on the value of other fields, you might want to take a look at the Form API #states property, it allows you to easily create conditional form fields with little code.

The typical steps involved:

  1. Create a new form or use hook_form_alter to change an existing form.
  2. Add an '#ajax' render element to a field that should trigger a callback function.
  3. Define name of the callback function and type of event that will use it.
  4. When the event is triggered by changing a form field's value the browser triggers an AJAX request
  5. Drupal receives the AJAX request and rebuilds the form. Using the information provided by the AJAX request data the form will have the required modification (one more element in a multi valued element, for example).
  6. After the form is rebuilt, the callback function is called to build the response to the AJAX callback.
  7. The callback function allows accessing the $form array and the FormStateInterface and must finally return a render array or some HTML markup or can execute an AJAX Command.

For a detailed workflow please check the Form API Internal Workflow page

Ajax Forms example

In the example below, we create a simple form with a select field that triggers an event whenever the user changes selection. The callback function fills the textbox element with the selected text. As we progress with the different kinds of callbacks we'll show different ways to dynamically update form fields and other elements as well as run custom AJAX commands.

When the form is submitted it simply displays all $form_state values using \Drupal::messenger().

Dynamic AJAX Forms with Drupal

Preparing a form field

You can use AJAX events on your own forms or attach your custom event to an existing field in another form that was created by core or a contrib module. Use hook_form_alter to attach AJAX events to existing forms.

Drupal AJAX Forms, Figure 1

<?php

namespace Drupal\ajaxfilters\Form;

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

/**
 * Provides a default form.
 */
class DefaultForm extends FormBase {

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'default_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    // Create a select field that will update the contents
    // of the textbox below.
    $form['example_select'] = [
      '#type' => 'select',
      '#title' => $this->t('Select element'),
      '#options' => [
        '1' => $this->t('One'),
        '2' => $this->t('Two'),
        '3' => $this->t('Three'),
        '4' => $this->t('From New York to Ger-ma-ny!'),
      ],
    ];

    // Create a textbox that will be updated
    // when the user selects an item from the select box above.
    $form['output'] = [
      '#type' => 'textfield',
      '#size' => '60',
      '#disabled' => TRUE,
      '#value' => 'Hello, Drupal!!1',      
      '#prefix' => '<div id="edit-output">',
      '#suffix' => '</div>',
    ];

    // Create the submit button.
    $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];

    return $form;
  }

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

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    // Display result.
    foreach ($form_state->getValues() as $key => $value) {
      \Drupal::messenger()->addStatus($key . ': ' . $value);
    }
  }

}

Drupal will auto-generate an ID for all Form elements and you can always view the rendered output to find out what ID Drupal has created. However, this is not future-proof and Drupal may, for whatever reason, change the ID and your callback will then fail to work. To overcome that the '#attributes' render element has been used to supply a specific HTML ID. This will be important in later steps.

Adding the AJAX event

In order to attach the AJAX callback, an ['#ajax'] tag has to be added to the desired form field. It defines the function that will be triggered to build the response to the AJAX request, the type of event and some other parameters like a throbber type. See the Ajax API docs for a detailed explanation of the render elements.

Add an AJAX callback to the select field, which will show the selected Text in the textbox:

// Create a select field that will update the contents
// of the textbox below.
$form['example_select'] = [
  '#type' => 'select',
  '#title' => $this->t('Example select field'),
  '#options' => [
    '1' => $this->t('One'),
    '2' => $this->t('Two'),
    '3' => $this->t('Three'),
    '4' => $this->t('From New York to Ger-ma-ny!'),
  ],
  '#ajax' => [
    'callback' => '::myAjaxCallback', // don't forget :: when calling a class method.
    //'callback' => [$this, 'myAjaxCallback'], //alternative notation
    'disable-refocus' => FALSE, // Or TRUE to prevent re-focusing on the triggering element.
    'event' => 'change',
    'wrapper' => 'edit-output', // This element is updated with this AJAX callback.
    'progress' => [
      'type' => 'throbber',
      'message' => $this->t('Verifying entry...'),
    ],
  ]
];

This adds an AJAX event that triggers the method myAjaxCallback() when the selection in the example select field changes.

Explanation of the #ajax tags used above:

  • callback => The function or class method to call when building the response to the AJAX request. Note the different notations you'll find when looking for examples on the net. If you created your own forms class as we did in the example above you're passing a class method as a callback and will have to use 'callback' => '::myAjaxCallback' or 'callback' => [$this, 'myAjaxCallback']. But if you instead used hook_form_alter(..) to change an existing form, you're very likely are passing a function that is defined in your own .module file. In that case only use 'callback' => 'myAjaxCallback'.
     
  • event => The event to trigger on; any valid DOM event for the element can be used, simply omit the 'on' portion of the event. 'onclick' => 'click', 'onkeyup' => 'keyup', 'onchange' => 'change', etc.
     
  • wrapper => The id of the element you wish to change. If your callback returns a render array or markup, the wrapper with this id is replaced with your result. 
     
  • progress => The indicator to use during the request so the user knows the page is waiting on a response from the server

A full list of known #ajax properties is described below.

The wrapper element is optional and useful only when returning HTML in the next step. This ID should match the ID on the HTML/Form element you want to act upon. In the above example, the 'output' input element was given an ID of 'edit-ouput' and so 'wrapper' reflects that same ID.

Note: The wrapper does not contain a hashtag prefix (#edit-output) as you would use in CSS. 

If you run this code now, you'll already see a Throbber icon appear when you select a value in the example select field, but you will find an error message in your JavaScript console: 

The website encountered an unexpected error. Please try again later.Symfony\Component\HttpKernel\Exception\HttpException: The specified #ajax callback is empty or not callable. in Drupal\Core\Form\FormAjaxResponseBuilder->buildResponse() (line 67 of core/lib/Drupal/Core/Form/FormAjaxResponseBuilder.php)

This happens because we haven't implemented our callback function yet.

Rebuilding the form

First, we will add a code to create a textfield that will be updated when the user selects an item from the select field. This way, when the form is rebuilt during the processing of the AJAX callback the textfield will get to right value. For this, we'll fetch the selected value of the select field from the $form_state interface and save it to the output textfield.

Here's is the part of the buildForm function related to the textfield: 

    // Create a textbox that will be updated
    // when the user selects an item from the select box above.
    $form['output'] = [
      '#type' => 'textfield',
      '#size' => '60',
      '#disabled' => TRUE,
      '#value' => 'Hello, Drupal!!1',      
      '#prefix' => '<div id="edit-output">',
      '#suffix' => '</div>',
    ];

  // If there's a value submitted for the select list let's set the textfield value.
  if ($selectedValue = $form_state->getValue('example_select')) {
      // Get the text of the selected option.
      $selectedText = $form['example_select']['#options'][$selectedValue];
      // Place the text of the selected option in our textfield.
      $form['output']['#value'] = $selectedText;
  }

This way the textfield element has the right value after the form rebuild. The callback function just needs to take this element and build the AJAX response accordingly.

Implement the callback function

Let's finally figure out a way to call our callback function that dynamically updates our form's textfield:

Drupal core calls our custom callback function using call_user_func_array($callback, [&$form, &$form_state, $request]); in FormAjaxResponseBuilder.php.

Possible return values:

There are different ways to respond to the ajax request, depending on what you want to achieve with your AJAX callback.

  • Render array => If you just want to update a field on your form you can simply return a render array, ie. $form['field_yourfieldname']. The resulting markup will be placed in the wrapper element, that was defined when attaching the ['#ajax'] render array to the form field.
     
  • Custom HTML markup => If you want to display some information on your form that is not related to a field on your form, you can return HTML Markup that will be placed in the wrapper element, that was defined when attaching the ['#ajax'] render array to the form field. Basically you also return a render array because you have to wrap your HTML markup in a render array return ['#markup' => '<h1>Hello</h1>'];.
     
  • AJAX Command => If you don't want to update your form at all but instead invoke an AJAX Command, you have to return an AjaxResponse object. 

Important: Make sure to ALWAYS return a response object or valid render array from your callback function. Returning NULL or an invalid render array can/will break your form. You will probably find an error message in your browser's JavaScript console:

TypeError: Argument 1 passed to Drupal\Core\Render\MainContent\AjaxRenderer::renderResponse() must be of the type array, null given, called in /web/core/lib/Drupal/Core/Form/FormAjaxResponseBuilder.php on line 89 in Drupal\Core\Render\MainContent\AjaxRenderer->renderResponse() (line 53 of /web/core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php).

Render Array

Dynamic Drupal AJAX Forms

The callback function just needs to return the textfield:

// Get the value from example select field and fill
// the textbox with the selected text.
public function myAjaxCallback(array &$form, FormStateInterface $form_state) {
  // Return the prepared textfield.
  return $form['output']; 
}

The returned array replaces the element with the ID specified by the wrapper element (#edit-output). This is important to remember when deciding what to return from the callback! Whatever was specified by the given ID is completely replaced by the new HTML! Make sure that the returned field also contains the id #edit-output or the next AJAX callback will not find a target to place the rendered code into.

HTML Markup

If you don't want to update the value of a form field but instead show some custom HTML markup on your form, you can also return HTML markup instead of a render array. Even though you can return raw HTML the return value must still be wrapped in a render array. Use ['#markup' => '<p>your markup</p>'] to return the response.

The returned markup entirely replaces the object with the id passed as wrapper in the #ajax render array, attached to the form field, so don't forget to add the id, that you specified as wrapper in the ['#ajax'] render array in the custom markup you are returning from your callback.

The example below will replace the output textfield that was originally placed on our form with a DIV element that holds the selected text. Take into account that this example is not something you would do in a real site. We have already updates the textfield in the buildForm method, so it makes no sense to generate a HTML code and return when we could just let the theme layer to render and return the textfield as we have done in the REnder Array example. However, for the shake of the example, we are doing this way in this case.

Drupal AJAX Forms, Figure 3

// Get the value from example select field and fill
// the textbox with the selected text.
public function myAjaxCallback(array &$form, FormStateInterface $form_state) {
  $markup = 'nothing selected';

  // Prepare our textfield. Check if the example select field has a selected option.
  if ($selectedValue = $form_state->getValue('example_select')) {
      // Get the text of the selected option.
      $selectedText = $form['example_select']['#options'][$selectedValue];
      // Place the text of the selected option in our textfield.
      $markup = "<h1>$selectedText</h1>";
  }

  // Don't forget to wrap your markup in a div with the #edit-output id
  // or the callback won't be able to find this target when it's called
  // more than once.
  $output = "<div id='edit-output'>$markup</div>";

  // Return the HTML markup we built above in a render array.
  return ['#markup' => $output];
}

AJAX Commands (AjaxResponse)

You can also choose to return an AjaxResponse that issues AJAX commands via the client-side jQuery library when your callback function is executed. You can choose from a range of existing AJAX commands, implement a custom AJAX command or just run some JavaScript code in one of your scripts.

The example callback function returns the updated textfield with the selected text and replace our original output textbox with it in the browser. Then it will show a modal dialogbox with the selected text from our select form element.

Drupal AJAX Forms, Figure 4

// Dont't forget to add the required namespaces at the beginning of your class/module.
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Ajax\OpenModalDialogCommand;

/**
 * An Ajax callback.
 */
public function myAjaxCallback(array &$form, FormStateInterface $form_state) {
  // Try to get the selected text from the select element on our form.
  $selectedText = 'nothing selected';
  if ($selectedValue = $form_state->getValue('example_select')) {
    // Get the text of the selected option.
    $selectedText = $form['example_select']['#options'][$selectedValue];
  }

  // Attach the javascript library for the dialog box command
  // in the same way you would attach your custom JS scripts.
  $dialogText['#attached']['library'][] = 'core/drupal.dialog.ajax';
  // Prepare the text for our dialogbox.
  $dialogText['#markup'] = "You selected: $selectedText";

  // If we want to execute AJAX commands our callback needs to return
  // an AjaxResponse object. let's create it and add our commands.
  $response = new AjaxResponse();
  // Issue a command that replaces the element #edit-output
  // with the rendered markup of the field created above.
  // ReplaceCommand() will take care of rendering our text field into HTML.
  $response->addCommand(new ReplaceCommand('#edit-output', $form['output']));
  // Show the dialog box.
  $response->addCommand(new OpenModalDialogCommand('My title', $dialogText, ['width' => '300']));

  // Finally return the AjaxResponse object.
  return $response;
}

Much like the previous example, a render array is created that will produce HTML that replaces the previous element specified by the ID passed into the ReplaceCommand() function. In this case, the hashtag must be included (#edit-output) to identify the element to be replaced. After that a popup dialog box is displayed.

You can find a list of all commands you can pass in the response here.

Execute custom Javascript in an AJAX callback

You could write a custom AJAX command, but you just might want to run a few lines of Javascript without the overhead. The AJAX InvokeCommand can be used to call jQuery methods:

Your AJAX callback handler:

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

/**
 * An Ajax callback.
 */
public function myAjaxCallback(array &$form, FormStateInterface $form_state) {
  $response = new AjaxResponse();
  $response->addCommand(new InvokeCommand(NULL, 'myAjaxCallback', ['This is the new text!']));
  return $response;
}

Your JavaScript code:

(function($) {
  // Argument passed from InvokeCommand.
  $.fn.myAjaxCallback = function(argument) {
    console.log('myAjaxCallback is called.');
    // Set textfield's value to the passed arguments.
    $('input#edit-output').attr('value', argument);
  };
})(jQuery);

Debugging AJAX Callback Functions

Kint can be used to debug AJAX callbacks, but that turns out to be a little tricky. Simply calling kint($form_state) from your callback function results in an AJAX error or a WSOD. But you can use an AjaxResponse to display the output of kint(..) on your form in order to inspect variables in your callback.

Basically you use Kint::dump(..) method to get the kint output into a variable and display it's content in a DIV on our form.

Before you can use @Kint::dump in your module you'll have to include the Kint class.  A good place to do that is your settings.local.php or settings.php. 

Add this to the end of your settings.php or settings.local.php. If you're putting it in settings.local.php also make sure that settings.local.php is included from within your settings.php. Make sure to rebuild your cache after that (drush cr).

settings.php or settings.local.php

// Include Kint class.
include_once(DRUPAL_ROOT . '/modules/contrib/devel/kint/kint/Kint.class.php');

// If debugging is very slow or leads to WSOD reduce the number
// of levels of information shown by Kint.
// Change Kint maxLevels setting:
if (class_exists('Kint')){
  // Set the maxlevels to prevent out-of-memory. Currently there doesn't seem to be a cleaner way to set this:
  Kint::$maxLevels = 4;
}

This code goes into your custom module:

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;

/**
 * Implements hook_form_alter().
 *
 * Add debug output container and Ajax callback.
 */
function YOUR_MODULE_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
  if ($form_id == 'your_form_id') {
    // Add empty container to the form. it gets filled with
    // the Kint output from our Ajax event callback.
    $form['debug'] = [
      '#type' => 'container',
      '#attributes' => [
        'id' => ['debug-out'],
      ],
    ];
  }

  // Here follows the code where you add the #ajax callback
  // to some field of this form. Let's say a taxonomy reference select box.
  $form['field_yourtaxonomy']['widget']['#ajax'] = [
    'event' => 'change',
    // Note how we declare the callback here. No :: and no [$this, 'myAjaxCallback']
    // because we are doing this from our .module file and not from within a class object.
    'callback' => 'your_module_ajax_callback',
  ];
}

// The callback function, triggered by changing selection of our taxonomy select box.
function your_module_ajax_callback(array &$form, FormStateInterface $form_state) {
  $response = new AjaxResponse();
  $debugOut = @Kint::dump($form_state);
  $response->addCommand(new ReplaceCommand('#debug-out', $debugOut ));
  return $response;
}

Example: Dynamic Textfield

Above examples are using a SELECT form field to trigger AJAX events. The same approach can be used to trigger callback functions when a user enters text in a textfield element.

<?php

namespace Drupal\my_module\Form;

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

/**
 * Class SearchForm.
 */
class SearchForm extends FormBase {

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'search_form';
  }

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

    // Prevent page reload when user hits enter key in the searchbox.
    $form['#attributes'] = [
      'onsubmit' => 'return false',
    ];

    // Define the searchfield.
    $form['search_product'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Search product'),
      '#description' => $this->t('Enter search text and hit enter key.'),
      '#maxlength' => 64,
      '#size' => 64,
      '#weight' => '0',
      // Attach AJAX callback.
      '#ajax' => [
        'callback' => '::updateSearchString',
        // Set focus to the textfield after hitting enter.
        'disable-refocus' => FALSE,
        // Trigger when user hits enter key.
        'event' => 'change',
        // Trigger after each key press.
        // 'event' => 'keyup'
        'progress' => [
          'type' => 'throbber',
          'message' => $this->t('Searching products ...'),
        ],
      ],
    ];

    // Optionally display a submit button.
    // $form['submit'] = [
    //   '#type' => 'submit',
    //   '#value' => $this->t('Submit'),
    // ];

    return $form;
  }

  /**
   * AJAX Callback for searchbar.
   *
   * Called when the text in the searchbox changes.
   * Calls custom Javascript command.
   *
   * @param array $form
   *   Nested array of form elements that comprise the form.
   * @param Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
   * @return Drupal\Core\Ajax\AjaxResponse
   *   Returns an AJAX response object.
   */
  public function updateSearchString(array &$form, FormStateInterface $form_state) {
    // Get value from search textbox.
    $searchText = $form_state->getValue('search_product');
    // Invoke the callback function.
    $response = new AjaxResponse();
    $response->addCommand(new InvokeCommand(NULL, 'MyJavascriptCallbackFunction', [$searchText]));
    return $response;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    // Do nothing.
  }

}

Full list of available #ajax properties

The #ajax property of a form element is an array. Here are the details of its known elements, all of which are optional:

  • callback: The callback to invoke to handle the server side of the Ajax event. More information on callbacks is under "Setting up a callback to process Ajax".
    If you use 'callback', your callback method is a function, which will receive the $form and $form_state from the triggering form. You can use $form_state to get information about the data the user has entered into the form. 
  • wrapper: The HTML 'id' attribute of the area where the content returned by the callback should be placed. Note that callbacks have a choice of returning content or JavaScript commands; 'wrapper' is used for content returns.
  • method: The jQuery method for placing the new content (used with 'wrapper'). Valid options are 'replaceWith' (default), 'append', 'prepend', 'before', 'after', or 'html'. See http://api.jquery.com/category/manipulation/ for more information on these methods.
  • effect: The jQuery effect to use when placing the new HTML (used with 'wrapper'). Valid options are 'none' (default), 'slide', or 'fade'.
  • speed: The effect speed to use (used with 'effect' and 'wrapper'). Valid options are 'slow' (default), 'fast', or the number of milliseconds the effect should run.
  • event: The JavaScript event to respond to. This is selected automatically for the type of form element; provide a value to override the default. See RenderElement::preRenderAjaxForm for the type-specific defaults. Especially note that the event for submit, button and image_button is "mousedown" (not "click") for example!
  • prevent: A JavaScript event to prevent when the event is triggered. For example, if you use event 'mousedown' on a button, you might want to prevent 'click' events from also being triggered.
  • disable-refocus: Disable automatic refocus after an ajax call.
  • progress: An array indicating how to show Ajax processing progress. Can contain one or more of these elements:
    • type: Type of indicator: 'throbber' (default), 'bar' or 'none'.
    • message: Translated message to display.
    • url: For a bar progress indicator, URL path for determining progress.
    • interval: For a bar progress indicator, how often to update it.
  • url: A \Drupal\Core\Url to which to submit the Ajax request. If omitted, defaults to either the same URL as the form or link destination is for someone with JavaScript disabled, or a slightly modified version (e.g., with a query parameter added, removed, or changed) of that URL if necessary to support Drupal's content negotiation. It is recommended to omit this key and use Drupal's content negotiation rather than using substantially different URLs between Ajax and non-Ajax.
    Note that if you use this url property, your route controller will be triggered with only the information you provide in the URL.

Further Reading and examples

Help improve this page

Page status: Needs review

You can: