This is the first post on Drupal Patterns exploring how we might build something. Charity Majors described writing as, "thinking on paper," on a recent Pragmatic Engineer podcast. She spoke about why space for thought is so important and helped me understand, at a deeper level, why I created this blog. Look for more posts exploring ideas that are still in formation as well as those that explore what we have built and how that works. I hope that you will join the conversation using one of the links below the post!
MessageCommand and the Ajax API
Dynamic message delivery in Drupal has been one of the tasks of our venerable Ajax API. Here is the current flow:
- A response that needs to include a message adds an instance of MessageCommand into its command stack.
- The command stack is packaged into an AjaxResponse which returns JSON to the client.
- The commands are processed as a queue, and the message command causes a Drupal.Message object to be created.
- Message markup is generated in JavaScript using Drupal.theme.message().
- The message is announced to assistive technology using Drupal.announce().
- The message element is appended to the selected wrapper. That wrapper is either the full status messages listing or an empty div rendered on the page for that purpose.
Goals for refactoring
Building parallel functionality
This is our standing goal for refactoring the Ajax API. We build functionally equivalent features alongside the existing subsystem. In this case we need messages generated by a request from HTMX to appear on the page, if status message display is enabled. We also need a way to get only the pending messages.
Messages are no longer an exception for theming
The community responded to moving dynamic messages completely into JavaScript with AJAX MessageCommand markup and styling differs from Theme default. That issue seeks solutions for themes to be able to style individual messages. There is dissatisfaction with needing to support javascript based theming in addition to Twig.
Overall JavaScript weight is reduced
The Ajax API message implementation requires a small amount of additional javascript for the message behaviors. The real weight is in the Drupal Ajax javascript and its jQuery dependency.
A Proposed Solution
Our objective is to build a dynamic message delivery replacement so that it works using only our HTMX integration and some CSS. We are about to commit HTMX 4 so I am taking advantage of a new feature in this design. I'll discuss the design and then walk through an implementation. If you see an improvement, please follow the issue link at the conclusion of this post and offer your ideas or code! If you are new to the Drupal community, create an account so you can comment on the issue.
Architecture
The bountiful expansion of CSS capabilities since the original message design enables us to create a conditional display of the message containers without JavaScript. The data structure for HTMX is HTML which means that the majority of the architectural changes are actually in templating and render classes. We will need:
- Standardized selectors for the container element of each group of message types and for the outer element that wraps the actual sequence of messages.
- CSS that visually hides the message type containers when they do not contain any actual messages. We don't need an empty div with modern CSS. These containers already have ARIA live region roles so rendering them makes them ready to announce new messages.
- The ability to render a single message into HTML.
- HtmxRenderer refactored for individual message delivery.
Implementation
Status messages HTML
Themes need creative freedom to structure their markup, so I propose using a few more data attributes in the status message structure: data-drupal-message-set on the element that contains an entire set messages and data-drupal-message-list-type on the immediate parent of the individual message elements. Here are the current and proposed templates in starterkit_theme followed by a discussion of the changes.
Current:
<div data-drupal-messages>
{% block messages %}
{% for type, messages in message_list %}
{%
set classes = [
'messages',
'messages--' ~ type,
]
%}
<div role="{{ type in ['error', 'warning'] ? 'alert' : 'status' }}" aria-label="{{ status_headings[type] }}"{{ attributes.addClass(classes)|without('role', 'aria-label') }}>
{% if status_headings[type] %}
<h2 class="visually-hidden">{{ status_headings[type] }}</h2>
{% endif %}
{% if messages|length > 1 %}
<ul class="messages__list">
{% for message in messages %}
<li class="messages__item">{{ message }}</li>
{% endfor %}
</ul>
{% else %}
{{ messages|first }}
{% endif %}
</div>
{# Remove type specific classes. #}
{% set attributes = attributes.removeClass(classes) %}
{% endfor %}
{% endblock messages %}
</div>Proposed:
<div data-drupal-messages>
{% for type, messages in message_list %}
<div role="{{ type in ['error', 'warning'] ? 'alert' : 'status' }}" aria-label="{{ status_headings[type] }}"{{ attributes|without('role', 'aria-label') }} data-drupal-message-set>
{% if status_headings[type] %}
<h2 class="visually-hidden">{{ status_headings[type] }}</h2>
{% endif %}
<ul data-drupal-message-list-type="{{ type }}">
{%- for message in messages %}
{{ message }}
{%- endfor -%}
</ul>
</div>
{% endfor %}
</div>First note the data attributes are added, in this case to an outer div and a ul tag. The conditional rendering is removed as the standard expects zero or more list items. The third change is very subtle: the addition of hyphen symbols in the beginning and end elements of the for-loop control structures. The hyphen is a white space control in Twig that removes whitespace including new lines. This is important for our CSS because when there are no messages we want to output
<ul class="messages__list" data-drupal-message-list-type="current-type"></ul>Which brings us to implementing the second item in the architecture, which is adding this to the CSS files for styling messages:
[data-drupal-message-set]:has([data-drupal-message-list-type]:empty) {
position: absolute !important;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
width: 1px;
height: 1px;
word-wrap: normal;
}This bit of CSS takes advantage of two modern selectors to deliver the same set of styles that is used in Drupal's visually-hidden class. The :has() pseudo-class takes a relative selector. When the relative selector matches, the pseudo-class is valid and its styles apply. The :empty pseudo-class matches nodes that have no children. Whitespace is a kind of text and creates a text node. Which is why we need to eliminate the whitespace inside the element that contains the messages. The goal here is not display the empty markup to sighted users but allow screen readers to parse the live region roles so that new messages cause the whole structure to appear visually and to be announced by the screen reader. I also wonder if the h2 should be styled display:none when the list is empty and if it's really a better experience to have the label value read out twice, once from aria-label and then from the heading tag.
Individual message rendering
Our ultimate goal is to send a single message back to the browser so that it is placed into the element with the appropriate data-drupal-message-list-type value. That means we need to be able to render a single message. The minimum change is to define a theme render mapping. These map an identifier to a list of expected variables with default values. The identifier becomes the template name. Drupal core maps these in ThemeCommonElements.
Both Registry::processExtension and SystemThemeHooks::theme call ThemeCommonElements::commonElements which returns all large associative array that defines these mappings. We add a single entry to this array:
'status_message_item' => [
'variables' => [
'message' => NULL,
'attributes' => [],
],
],This enables us to create status-message-item.html.twig
{#
/**
* @file
* Default theme implementation for an individual status message.
*
* Available variables:
* - message: The message content.
* - attributes: HTML attributes for the containing li element.
*
* @ingroup themeable
*/
#}
<li {{ attributes.addClass('messages__item') }}>{{ message }}</li>
To use this in normal status message rendering we also need to refactor the StatusMessages render element class. Render element classes are more complex than the mappings we just explored. They implement Drupal's plugin pattern to pair logic with their data structure.
We need to refactor the renderMessages method. The current implementation gets an array of messages and passes that array to the status messages template we walked through above. We alter that to loop the messages here and pair them with the render mapping we just defined:
$messageItems = [
MessengerInterface::TYPE_ERROR => [],
MessengerInterface::TYPE_WARNING => [],
MessengerInterface::TYPE_STATUS => [],
];
foreach ($messages as $type => $messageList) {
foreach ($messageList as $message) {
$item = [
'#theme' => 'status_message_item',
'#message' => $message,
];
$messageItems[$type][] = $item;
}
}
// Render the messages.
$render = [
'#theme' => 'status_messages',
'#message_list' => $messageItems,
'#status_headings' => [
'status' => t('Status message'),
'error' => t('Error message'),
'warning' => t('Warning message'),
],
];
return $render;We then send the nested array of render mappings, which Drupal commonly calls a render array, as the message list.
Dynamic Messaging through HTMX
We now have all the prerequisites to refactor HtmxRenderer to send HTML fragments of the individual messages. For this we get to use a new feature in HTMX 4: hx-partial. The markup looks like a web component, but the custom tag never makes it to the DOM in the user's browser. It is used as a container to hold some HTMX properties. The custom tag is parsed by HTMX and its contents are swapped into the DOM based on the attributes added to the hx-partial tag. We replace the usual rendering of status messages in this renderer's response with a series of fragments to insert these individual messages into the requesting page.
foreach ($pendingMessages as $type => $messageList) {
foreach ($messageList as $message) {
$messages[] = [
'#type' => 'inline_template',
'#template' => <<<HTMX_MESSAGE
<hx-partial hx-target='[data-drupal-message-list-type="{{ type }}"]' hx-swap="beforeend">
{{item}}
</hx-partial>
HTMX_MESSAGE,
'#context' => [
'type' => $type,
'item' => [
'#theme' => 'status_message_item',
'#message' => $message,
],
],
];
}
}
Refactoring for HTMX
Building solutions for HTMX means thinking about and crafting HTML because HTMX is a tool for building hypermedia with HTML. Which is why the solution we have explored is almost entirely about refactoring how we render and style status message markup. Drupal builds software in community. Whether you think this is the right solution, have a few suggestions, or an entirely different idea, I encourage you to use the issue link below and participate in the conversation.
We would also truly benefit from your participation in the Htmx Initiative. If you are not already a participant in Drupal Slack then create an account. Join us in the #htmx channel. Look over the active initiative issues and help us bring latest hypermedia technology to the latest version of Drupal!