Drupal's process of returning a response is founded on Symfony's HttpKernel Component which provides a number of events that allows Drupal code to modify or replace the response. In this post I'll examine two ways that this event system is used to return a response written to be consumed by the HTMX library.
The architecture
I'll break this down in more detail below but we are looking at two event subscribers, that is classes that implement EventSubscriberInterface. A standard page in Drupal is rendered by the HtmlRenderer. Our simplified HTML page is provided by HtmxRenderer. Both of these event subscribers have logic that we will examine below that diverts the rendering process to HtmxRenderer.
Subscribed events
Event subscribers declare their subscriptions in ::getSubscribedEvents() by mapping event identifiers to a class method. Subscribers may include in that mapping an integer priority to place their processing of the event before or after other subscriptions with the default priority automatically set to zero. Both of these classes declare a subscription to the kernel.view event. MainContentViewSubscriber maps this event to ::onViewRenderArray() with the default priority. HtmxContentViewSubscriber maps the event to ::renderHtmxResponse() with a priority of 100, so it will receive and process the event before MainContentViewSubscriber.
Route options
Route options provide additional information about how requests to the given route interact with Drupal. HtmxContentViewSubscriber watches for requests rendering on routes configured with the _htmx_route option set to true. When a request comes through for such a route, a render array has been produced, and the expected response is HTML, the request is turned into a response by HtmxRenderer and that response is passed to the event in a setter method. Setting the request in the event stops propagation of the kernel.view event and the response from HtmxRenderer is returned.
Wrapper formats
Drupal has a incredible caching system. Matt Glaman's excellent book, Understanding Drupal: A Complete Guide to Caching Layers, covers every level of that system. For this post, let's just specify that one of the aspects of the system is to specify how the cacheable thing can vary. That's very important for this discussion as MainContentViewSubscriber is sending requests to different renderers. We only show two in the diagram but there are also renderers for the JSON response used by the old Ajax API, and for various types of dialogs. MainContentViewSubscriber looks for a query parameter (_wrapper_format) and uses the value of that parameter to select the renderer, but it also communicates to the caching system that the response varies by the value of that parameter. This allows another request to come in without the wrapper format set and get the full page render returned from cache and another with _wrapper_format=drupal_htmx on the same path that gets sent to HtmxRenderer for a simplified response.
A closer look at HtmxRenderer
Simple page
A typical HTML document returned by Drupal includes a variety of content that is rendered above, below, or along side the main content of the page. Some of that content will be pulled from cache but some may be dynamic and recalculated for every page. Our objective with HtmxRenderer is to return the simplest document possible. We build return the essentials: status messages, the main content, and any CSS or JavaScript needed to display the content on the requesting page.
Status messages
Status messages are returned in Drupal 11 by including a StatusMessages element. We left the selection and swapping of these messages to the implementing code in this initial integration. That integration would likely use hx-select-oob. For Drupal 12 we are working on a revision that dynamically swaps status messages as part of the response. I'll outline the design and implementation of this improvement in an upcoming post.
Main content
We include the main content of the response immediately after the status messages. No additional wrapper markup is added, only the content as it was produced by the code that services the requested route.
Here's the full structure of a response from HtmxRenderer:
<!doctype html>
<html>
<head>
<meta name="robots" content="noindex">
<title>Title from main content</title>
<css-placeholder token="token-value">
<js-placeholder token="token-value">
<js-bottom-placeholder token="token-value">
</head>
<body>
<!-- Status messages -->
<!-- Main content -->
</body>
</html>The placeholders will be replaced by HtmlResponseAttachmentsProcessor and moved into the structure of the requesting page by our integration JavaScript. Note that our javascript integration is expected to change in the next version of Drupal due to improvements in HTMX 4.
Providing a simple response for HTMX requests that works with Drupal's caching system was a straightforward feature to implement thanks to the contributors who built the render and routing systems to be extendable and flexible!