Using the Django messages framework with HTMX's OOB swaps

In my previous article, I showed you how to integrate the Django messages framework with HTMX thanks to the HX-Trigger header and some JavaScript code. Today, we’ll see an alternative (and probably better) way to do this using out-of-band swaps.

Django messages framework with HTMX's OOB swaps

I won’t do a tutorial because it would be too similar to my previous article; instead, I’ll focus on the key elements of this pattern. You can find the corresponding source code in the oob branch of the dedicated GitHub repository.

You can find an extended version of this article on YouTube.

Overview

Here is how the previous technique works:

  1. It all starts with a button with the hx-get attribute.
  2. When the user clicks on this button, HTMX performs a GET request which triggers the execution of a Django view.
  3. The view creates an HTTP response and publishes one or more messages.
  4. The response is intercepted by our middleware.
  5. The middleware pulls the messages and stores them in the HX-Trigger header of the response.
  6. The response goes over the wire and is received by HTMX.
  7. HTMX updates the page with the response body.
  8. When it sees the HX-Trigger header, it calls a JavaScript function.
  9. This function creates the toasts and adds them to the DOM.
  10. Finally, the browser displays the toasts along with the updated page.

Django messages framework with HTMX - version 1 with HX-Trigger

Now, let’s see how the new version works.

  1. Everything before the middleware stays the same.
  2. The middleware still pulls the messages, but instead of storing them in the HX-Trigger header, it appends the toast divs to the response’s body.
  3. The response goes over the wire and is received by HTMX.
  4. HTMX updates the page and injects the toast divs into the DOM.
  5. Finally, the browser displays the toasts along with the updated page.

Django messages framework with HTMX - version 2 with OOB swaps

As you see, we don’t need much JavaScript with this new version because we piggybacked the toasts to the page. This is what HTMX calls out-of-band swaps. They are “out of band” because they are not part of the primary swap; instead, they target specific elements in the DOM. This technique is more in line with HTMX’s philosophy of rendering the HTML in the backend.

What is an out-of-band swap?

In this graphic, I tried to represent the primary swap and the OOB swap. The primary swap targets the element designated by the hx-target (or the element that triggered the request if hx-target is not set). The OOB swap includes all the elements with the hx-swap-oob attribute and targets the elements with the same ids.

In the following sections, we’ll see each piece of the puzzle.

The page template

The element that triggers the HTMX request must have the hx-get, hx-post, hx-put, hx-patch, or hx-delete attribute. By default, the trigger element is also the target of the HTMX swap, but we can override this behavior by adding the hx-target attribute.

In this example, I used a button as the trigger and a paragraph as the target.

<button hx-get="/message" hx-target="#main">Emit message</button>
<p id="main">Lorem ipsum</p>

The Django view

The view pushes a message to the messages framework and returns the content for the paragraph:

from django.shortcuts import render
from django.contrib import messages

def message(request):
  messages.info(request, "Hello World!")
  return render(request, "lorem.html")

Don’t worry about the lorem.html template; it is just random text.

The Middleware

The middleware intercepts the responses returned by all Django views, so we first check that the response corresponds to an HTMX request. We must also check the response status to ensure it’s not a redirection because HTMX could not intercept the response. Similarly, we must ignore “soft” redirections triggered by HX-Redirect because HTMX ignores swaps in this case.

Once these three checks pass, we append the toast to the response body.

from django.contrib.messages import get_messages
from django.template.loader import render_to_string
from django.utils.deprecation import MiddlewareMixin

class HtmxMessageMiddleware(MiddlewareMixin):
  def process_response(self, request, response):
    if (
      "HX-Request" in request.headers
      and
      not 300 <= response.status_code < 400
      and
      "HX-Redirect" not in response.headers
    ):
      response.write(
        render_to_string(
          "toasts.html",
          {"messages": get_messages(request)},
        )
      )
    return response

As you can see, we render the HTML for the toasts using a regular Django template to which we provide the list of messages.

The toasts template

Here is the toast template:


<div id="toasts" hx-swap-oob="afterbegin">
  {% for message in messages %}
  <div class="toast {{ message.tags }}">
    <div class="toast-body">{{ message.message }}</div>
  </div>
  {% endfor %}
</div>

As you can see, it’s the same code you’d use to render Bootstrap toasts in a classic Django app except for the hx-swap-oob attribute on the container. This attribute tells HTMX to exclude this element from the primary swap and instead swap it with the element with the id toasts. The value afterbegin tells HTMX to insert this div’s content after the existing div’s opening tag. In other words, HTMX will insert the new toasts before the existing ones. This way, the new toasts will appear at the top. You could use the value beforeend to put the new toasts at the bottom.

The JavaScript code

Django renders the toasts on the server side, so one might think that we don’t need any JavaScript. Unfortunately, that’s not so simple.

Bootstrap toasts are initially hidden, and they will only show if we initialize them with JavaScript. For each toast, we must instantiate the bootstrap.Toast class and call its show() method. We must do this initialization step on the initial page load and for subsequent partial loads.

Fortunately, HTMX provides a helper function to do this: we can call htmx.onLoad() to register a callback function that will be called on the initial page load and for any later swap (including OOB swaps).

htmx.onLoad(() => {
  htmx.findAll(".toast").forEach((element) => {
    const toast = new bootstrap.Toast(element)
    toast.show()
  })
})

That’s a first step, but this code has a problem: not only does it show the newly added toasts, but it also resurrects old toasts!

HTMX passes the newly loaded element to the callback, so theoretically, we should be able to target only the newly loaded toasts. In practice, however, I could not find an elegant way to do it. Instead, I call bootstrap.Toast.getInstance() to get the instance of the bootstrap.Toast class for the toasts that are already initialized. If no instance exists, I create a new one and call show(). As a bonus, I also delete the old hidden toasts.

htmx.onLoad(() => {
  htmx.findAll(".toast").forEach((element) => {
    let toast = bootstrap.Toast.getInstance(element)
    if (!toast) {
      const toast = new bootstrap.Toast(element)
      toast.show()
    } else if (!toast.isShown()) {
      toast.dispose()
      element.remove()
    }
  })
})

Compatibility with the modal form pattern

Three articles ago, I presented a pattern to put Django forms in modal dialogs using HTMX.

This pattern uses an empty response (with the status code 204, but 200 works too) as a trigger to hide the dialog. A JavaScript hook listens to the htmx:beforeSwap event. The event handler checks if the target is the modal dialog and if the response is empty, in which case, it disables the swap (so the modal remains on screen during the fade-out animation) and hides the modal.

Unfortunately, this “empty response” trick doesn’t work with OOB swaps because the response is not empty anymore: it contains the toasts.

To work around this issue, we need another way to disable the primary swap and hide the modal. We can disable the swap by adding HX-Reswap: none in the response headers, and we can use HX-Trigger to raise a “hide modal” event. For example, the “movie edit” view now contains a statement like this:

return HttpResponse(
  headers={
    'HX-Trigger': '{"movieListChanged":true,"hideModal":true}',
    'HX-Reswap': "none",
  }
)

It works but is quite inelegant. Hopefully, I’ll find a better solution someday. Until then, you can check the complete implementation in the branch messages-framework-oob in the dedicated repository.

Conclusion

Honestly, I still haven’t fully made up my mind: is this technique superior to the one using HX-Trigger?

Sure, it seems better since we removed the clumsy JavaScript and simplified the template. However, we carelessly appended content to the response body, and I wonder if this is always possible. Moreover, the issue with empty responses bothers me because I really like this trick. For these reasons, I still use the old technique on my projects and don’t plan to upgrade them yet.

And you? What do you think of this technique? Please let me know in the comments.