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.
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:
- It all starts with a button with the
hx-get
attribute. - When the user clicks on this button, HTMX performs a
GET
request which triggers the execution of a Django view. - The view creates an HTTP response and publishes one or more messages.
- The response is intercepted by our middleware.
- The middleware pulls the messages and stores them in the
HX-Trigger
header of the response. - The response goes over the wire and is received by HTMX.
- HTMX updates the page with the response body.
- When it sees the
HX-Trigger
header, it calls a JavaScript function. - This function creates the toasts and adds them to the DOM.
- Finally, the browser displays the toasts along with the updated page.
Now, let’s see how the new version works.
- Everything before the middleware stays the same.
- 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. - The response goes over the wire and is received by HTMX.
- HTMX updates the page and injects the toast divs into the DOM.
- Finally, the browser displays the toasts along with the updated page.
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.
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.