AJAX Commands: Taking Full Control of the DOM from PHPfor Drupal 11 , and 10

Last updated :  

Last lesson, a form's AJAX callback returned a plain render array, and Drupal quietly wrapped it in an AjaxResponse for you behind the scenes. That worked because we only needed to replace one element. This lesson pulls back the curtain on that wrapping — and shows you how to take full control when a single click needs to do more than one thing to the page.

We're also switching contexts slightly: instead of a form, this example is a plain link, wired up for AJAX entirely by hand. That's a deliberately different starting point, because outside of the Form API, none of AJAX's usual conveniences happen automatically — which makes it the perfect example for seeing every moving part explicitly.

What you'll learn in this lesson

  • What an AjaxResponse object actually is, and how it differs from returning a plain render array
  • How to make an ordinary link AJAX-enabled using the use-ajax class
  • Why AJAX outside a form needs one extra step that AJAX inside a form gets for free
  • The full family of built-in AJAX command classes, and what each one does in the browser

The source file

Path (relative to the Examples module's root): modules/ajax_example/src/Controller/AjaxExampleController.php

<?php

namespace Drupal\ajax_example\Controller;

use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\AppendCommand;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Url;
use Drupal\examples\Utility\DescriptionTemplateTrait;
use Symfony\Component\HttpFoundation\Response;

/**
 * Controller routines for AJAX example routes.
 */
class AjaxExampleController extends ControllerBase {

  use DescriptionTemplateTrait;

  /**
   * {@inheritdoc}
   */
  protected function getModuleName() {
    return 'ajax_example';
  }

  /**
   * Demonstrates a clickable AJAX-enabled link using the 'use-ajax' class.
   *
   * Because of the 'use-ajax' class applied here, the link submission is done
   * without a page refresh.
   *
   * When using the AJAX framework outside the context of a form or a renderable
   * array of type 'link', you have to include ajax.js explicitly.
   *
   * @return array
   *   Form API array.
   *
   * @ingroup ajax_example
   */
  public function renderLinkRenderableArray() {
    $build['my_div'] = [
      '#markup' => $this->t('
The link below has been rendered as an element with the #ajax property, so if
javascript is enabled, ajax.js will try to submit it via an AJAX call instead
of a normal page load. The URL also contains the "/nojs/" magic string, which
is stripped if javascript is enabled, allowing the server code to tell by the
URL whether JS was enabled or not, letting it do different things based on that.'),
    ];
    // We'll add a nice border element for our demo.
    $build['ajax_link'] = [
      '#type' => 'details',
      '#title' => $this->t('This is the AJAX link'),
      '#open' => TRUE,
    ];
    // We build the AJAX link.
    $build['ajax_link']['link'] = [
      '#type' => 'link',
      '#title' => $this->t('Click me'),
      // We have to ensure that Drupal's Ajax system is loaded.
      '#attached' => ['library' => ['core/drupal.ajax']],
      // We add the 'use-ajax' class so that Drupal's AJAX system can spring
      // into action.
      '#attributes' => ['class' => ['use-ajax']],
      // The URL for this link element is the callback. In our case, it's route
      // ajax_example.ajax_link_callback, which maps to ajaxLinkCallback()
      // below. The route has a /{nojs} section, which is how the callback can
      // know whether the request was made by AJAX or some other means where
      // JavaScript won't be able to handle the result. If the {nojs} part of
      // the path is replaced with 'ajax', then the request was made by AJAX.
      '#url' => Url::fromRoute('ajax_example.ajax_link_callback', ['nojs' => 'nojs']),
    ];
    // We provide a DIV that AJAX can append some text into.
    $build['ajax_link']['destination'] = [
      '#type' => 'container',
      '#attributes' => ['id' => ['ajax-example-destination-div']],
    ];
    return $build;
  }

  /**
   * Callback for link example.
   *
   * Takes different logic paths based on whether Javascript was enabled.
   * If $type == 'ajax', it tells this function that ajax.js has rewritten
   * the URL and thus we are doing an AJAX and can return an array of commands.
   *
   * @param string $nojs
   *   Either 'ajax' or 'nojs. Type is simply the normal URL argument to this
   *   URL.
   *
   * @return string|array
   *   If $type == 'ajax', returns an array of AJAX Commands.
   *   Otherwise, just returns the content, which will end up being a page.
   */
  public function ajaxLinkCallback($nojs = 'nojs') {
    // Determine whether the request is coming from AJAX or not.
    if ($nojs == 'ajax') {
      $output = $this->t("This is some content delivered via AJAX");
      $response = new AjaxResponse();
      $response->addCommand(new AppendCommand('#ajax-example-destination-div', $output));

      // See ajax_example_advanced.inc for more details on the available
      // commands and how to use them.
      // $page = array('#type' => 'ajax', '#commands' => $commands);
      // ajax_deliver($response);
      return $response;
    }
    $response = new Response($this->t("This is some content delivered via a page load."));
    return $response;
  }

}

How it works

The imports

use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\AppendCommand;
use Symfony\Component\HttpFoundation\Response;
  • AjaxResponse — Drupal's dedicated HTTP response class for AJAX. It holds a queue of command objects and serialises them to JSON for the browser. Under the hood it extends Symfony's JsonResponse.
  • AppendCommand — one of many command classes living in Drupal\Core\Ajax\. Each command class maps directly to one jQuery/JavaScript DOM operation; this one maps to jQuery's .append().
  • Response — the plain Symfony response class, used for the non-JavaScript fallback path further down.

Building the page: attaching the AJAX library

'#attached' => ['library' => ['core/drupal.ajax']],

Here's the extra step this lesson's intro mentioned. When you use #ajax on a Form API element, Drupal quietly loads ajax.js for you. Outside a form — on a plain link like this one — nothing loads it automatically. Forget this line, and clicking the link performs a completely ordinary full-page navigation, with no error to tell you why.

The use-ajax class

'#attributes' => ['class' => ['use-ajax']],

This is the actual front-end trigger. Once ajax.js is present on the page, it watches for any link or button carrying the CSS class use-ajax. When it sees a click on one, it intercepts the event and fires an XHR request instead of letting the browser navigate normally.

The /nojs/ URL trick, applied to a link

'#url' => Url::fromRoute('ajax_example.ajax_link_callback', ['nojs' => 'nojs']),

This is the same graceful-degradation pattern from the first lesson in this topic, applied to a link instead of a form route. The link's default URL parameter is literally the string 'nojs'. When ajax.js intercepts the click, it rewrites that segment to 'ajax' before firing the XHR — giving the server-side callback a reliable signal about whether JavaScript actually ran.

The empty destination container

$build['ajax_link']['destination'] = [
  '#type' => 'container',
  '#attributes' => ['id' => ['ajax-example-destination-div']],
];

An empty <div id="ajax-example-destination-div">, rendered on the page with nothing inside it yet. The AJAX command you're about to see targets this exact element by that ID, and injects content into it.

ajaxLinkCallback() — two branches, one method

This method has to handle both possible outcomes: JavaScript ran, or it didn't.

$response = new AjaxResponse();
$response->addCommand(new AppendCommand('#ajax-example-destination-div', $output));
return $response;

This is the AJAX path, taken when $nojs === 'ajax'. Three things happen: an empty AjaxResponse is created (think of it as a blank list of instructions for the browser); addCommand() queues up an AppendCommand, which takes a jQuery selector and the content to insert; and returning the response causes Drupal to serialise the whole queue to JSON, which ajax.js reads and executes against the live DOM.

$response = new Response($this->t("This is some content delivered via a page load."));
return $response;

This is the fallback path. If JavaScript never rewrote the URL, $nojs is still the literal string 'nojs', so this branch runs instead — returning an ordinary Symfony Response as a full page. The visitor still gets the content; they just get it the old-fashioned way.

Chaining multiple commands

Nothing stops you from queueing more than one command on the same response — this is exactly what you'd reach for if a single click needed to update two different parts of the page:

$response = new AjaxResponse();
$response->addCommand(new HtmlCommand('#some-wrapper', $newHtml));
$response->addCommand(new InvokeCommand('#other-element', 'addClass', ['highlight']));
$response->addCommand(new AppendCommand('#log', 'Action completed.'));
return $response;

Every command class implements CommandInterface, whose single render() method produces the associative array Drupal serialises to JSON. The command key inside that array is what tells ajax.js which registered JavaScript handler to run.

The command classes available in Drupal core

ClassjQuery/JS equivalentDescription
AppendCommand.append()Inserts content at the end of matched elements
PrependCommand.prepend()Inserts content at the start of matched elements
HtmlCommand.html()Replaces the inner HTML of matched elements
ReplaceCommand.replaceWith()Replaces matched elements themselves, wrapper included
RemoveCommand.remove()Removes matched elements from the DOM
AfterCommand.after()Inserts content after matched elements
BeforeCommand.before()Inserts content before matched elements
CssCommand.css()Sets CSS properties on matched elements
InvokeCommand.method()Calls any jQuery method on matched elements
RedirectCommandwindow.locationRedirects the browser to a new URL
AlertCommandalert()Shows a JavaScript alert dialog
OpenModalDialogCommandDrupal dialogOpens content in a modal dialog

Every single one follows the exact same shape: instantiate it with a selector plus whatever content or arguments it needs, then hand the instance to $response->addCommand().

See it for yourself

Click "Click me" on the live example, and watch text appear inside the empty box below it — nothing else on the page moves.

The AJAX link example after clicking Click me, showing appended content with no page reload

"This is some content delivered via AJAX" is the exact string built inside ajaxLinkCallback(), appended into #ajax-example-destination-div by the AppendCommand you just read. Disable JavaScript in your browser and click the same link again — you'll still get the content, just as the plain-text fallback response instead.

Quick check: which single line is responsible for AJAX working at all on a plain link like this one, when it would otherwise not be needed on a Form API element? (Answer: '#attached' => ['library' => ['core/drupal.ajax']] — outside a form, nothing attaches ajax.js for you.)

Key takeaways

  • AjaxResponse is Drupal's dedicated AJAX response class; it holds a queue of command objects and serialises them to JSON for ajax.js to execute.
  • Every AJAX command class implements CommandInterface and maps to a specific jQuery/JavaScript DOM operation, identified by a CSS selector plus content or arguments.
  • Outside a Form API form, you must explicitly attach core/drupal.ajax via #attached — it isn't loaded automatically for plain links or custom render elements.
  • The use-ajax CSS class is what tells ajax.js to intercept a click and fire an XHR request instead of a normal navigation.
  • You can queue multiple commands on one AjaxResponse by calling addCommand() repeatedly — several DOM updates in a single round trip.
  • The {nojs} URL-rewriting pattern works identically for links as it does for forms, giving you the same graceful-degradation guarantee everywhere in the AJAX system.

Coming up next

You now know how Drupal routes AJAX requests, how a callback updates the page, and the full vocabulary of commands available for more elaborate updates. That's everything you need to build genuinely interactive UI — but interactive UI is only half the job of a real module. In the next topic, we'll shift from updating what a visitor sees to actually storing and retrieving data, using Drupal's Database API.