Dynamic messages with HTMX and Alpine.js
djangopythonweb developmenthtmxalpinejsA common requirement is returning "flash" messages from the server when the user successfully - or unsuccessfully - completes an action. The Django messages framework provides a simple interface to render messages. However, it's not so clear how to do this with HTMX.
Getting started #
To enable messages you need to ensure that the messages framework is enabled in your Django project.
We'll also need the django-htmx library:
pip install django-htmx
Follow the instructions on adding django-htmx
to your project.
Our application #
For our example, we have these two Django views:
from django.contrib import messages
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
def index(request: HttpRequest) -> HttpResponse:
"""Front page"""
return render(request, "index.html")
def send_message(request: HttpRequest) -> HttpResponse:
"""Just sends an OK message back to the user."
messages.success(request, "All OK!")
return render(request, "_send_button.html", {"message_sent": True})
Our very simple index template index.html
looks like this:
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Messages demo</title>
<meta name="description" content="HTMX django messages demo">
<meta name="keywords" content="htmx django">
<link rel="stylesheet" href="{% static 'index.css' %}">
<script src="https://unpkg.com/[email protected]"
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
crossorigin="anonymous"></script>
<script defer
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
</head>
<body>
{% include "_messages.html" %}
<main>
{% include "_send_button.html" %}
</main>
</body>
</html>
This page includes the CDNs for HTMX and Alpine and a couple include
templates.
The template _send_button.html
looks like this:
<button hx-post="{% url 'send_message' %}"
hx-swap="outerHTML"
hx-target="this"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
class="btn btn-lg">
{% if message_sent %}
Send again
{% else %}
Send message
{% endif %}
</button>
The button triggers an HTMX action: it will send a POST to our send_message
view, which should then just re-render this template, swapping out the contents when a message is sent. Note that we need to include the CSRF token in the header or Django will respond with a 403 Forbidden
response.
Finally we need to render the messages in _messages.html
:
<div id="messages" class="messages">
{% if messages %}
<ul>
{% for message in messages %}
<li class="message message-{{ message.tags }}"
role="alert">{{ message.message }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
This renders the messages. The messages
should be available in the template context if you have included the correct middleware and context processor when setting up the Messages framework (see above), so when calling messages.success(request, "All OK!")
in our view, it should be rendered correctly.
Rendering messages in HTMX #
So far so good! However, when we run the application and click the "Send message" button, although the button text changes, we don't see any messages. What's wrong?
The problem is that HTMX will only render content into the specified hx-target
, in this case the button itself, and our view only re-renders the _send_button.html
template. Somehow we need to inject the messages into our POST
response, without re-rendering the entire page.
However HTMX has an answer for this problem: hx-swap-oob
("OOB" means "Out of Band"). This handy attribute allows us to "side-load" additional snippets of content into our response, allowing us to update any other part of the page in addition to whatever we want to inject into the DOM node specified by hx-target
.
Let's add this to our _messages.html
template:
<div id="messages" class="messages"
{% if hx_oob %}hx-swap-oob="true"{% endif %}>
{% if messages %}
<ul>
{% for message in messages %}
<li class="message message-{{ message.tags }}"
role="alert">{{ message.message }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
Note that when using hx-swap-oob
you should always include the unique id
of the node you want to swap.
However, we somehow need to render this template to our final response. Here's one way to do it:
# add this to imports:
from django.template.loader import render_to_string
def send_message(request: HttpRequest) -> HttpResponse:
"""Just sends an OK message back to the user."
messages.success(request, "All OK!")
response = render(request, "_send_button.html", {"message_sent": True})
response.write(
template_name="_messages.html",
context={"hx_oob": True},
request=request,
)
return response
Et voilĂ :
When clicking the button, you should now see the 'All OK!' message at the top of the screen. We append another rendered template to our Django HttpResponse
instance using HttpResponse.write()
, and insert the hx-swap-oob
attribute. HTMX will then inject the rendered messages into the messages
node.
This works well, but in a large application writing this boilerplate each time is quite painful. We could wrap this into a function, for example:
def inject_messages(response: HttpResponse) -> HttpResponse:
response.write(
template_name="_messages.html",
context={"hx_oob": True},
request=request,
)
return response
And then in our view:
def send_message(request: HttpRequest) -> HttpResponse:
"""Just sends an OK message back to the user."
messages.success(request, "All OK!")
return inject_messages(render(request, "_send_button.html", {"message_sent": True}))
This could be done slightly better using a decorator:
import functools
from django.contrib.messages import get_messages
_hx_redirect_headers = frozenset(
{
"HX-Location",
"HX-Redirect",
"HX-Refresh",
}
)
def inject_messages(view: Callable) -> Callable:
"""Injects HTMX messages into response. """
@functools.wraps(view)
def _wrapper(request: HttpRequest, *args, **kwargs) -> HttpResponse:
response = view(request, *args, **kwargs)
if not request.htmx:
return response
if set(request.headers) & _hx_redirect_headers:
return response
if get_messages(request)
response.write(
template_name="_messages.html",
context={"hx_oob": True},
request=request,
)
return response
return _wrapper
And our view again:
@inject_messages
def send_message(request: HttpRequest) -> HttpResponse:
"""Just sends an OK message back to the user."
messages.success(request, "All OK!")
return render(request, "_send_button.html", {"message_sent": True})
There is a bit more logic in our decorator. We don't want to append the messages if we are doing a full page render or redirect, as the messages will get rendered anyway. Therefore we check if the request has an HX-Request
header (it won't if it's a non-HTMX request) or if it has an HTMX header that tells the browser to reload or re-render the entire page. See here for a reference of the various HTMX request headers.
Note that the django-htmx
middleware adds the htmx
attribute to our request
, and request.htmx
will always be False
(or false-y) if the HX-Request
header is absent.
We call get_messages()
to check if we do have any messages. This function allows us to check if there are any messages, without removing them from the session.
This is an improvement, but still bug-prone: we have to remember to include this decorator whenever we add a message in our view, and sooner or later we'll forget to do so (maybe when adding success or failure messages to an existing view) and wonder why our message doesn't show up.
Instead we can use middleware. Middleware executes with every request, so we can be assured this will work whether the view has messages or not. There's a bit of extra overhead compared to a decorator as it is always going to be called with every view, but in this case we don't have any expensive calls (like database operations) so it's probably worth it to avoid this particular source of bugs.
Thankfully, we've written most of the logic in our decorator, so it's not much more work to turn it into middleware:
class HtmxMessagesMiddleware:
"""Adds messages to HTMX response"""
_hx_redirect_headers = frozenset(
{
"HX-Location",
"HX-Redirect",
"HX-Refresh",
}
)
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
self.get_response = get_response
def __call__(self, request: HttpRequest) -> HttpResponse:
"""Middleware implementation"""
response = self.get_response(request)
if not request.htmx:
return response
if set(response.headers) & self._hx_redirect_headers:
return response
if get_messages(request):
response.write(
render_to_string(
template_name="_messages.html",
context={"hx_oob": True},
request=request,
)
)
return response
You will need to add this middleware to the MIDDLEWARE
list in your settings. It should be placed after django_htmx.middleware.HtmxMiddleware
and django.contrib.messages.middleware.MessageMiddleware
.
Now we can revert back to our original version of the view, as we don't need that extra logic for rendering the messages:
def send_message(request: HttpRequest) -> HttpResponse:
"""Just sends an OK message back to the user."""
messages.success(request, "All OK!")
return render(request, "_send_button.html", {"message_sent": True})
There is one annoyance remaining. When rendering the message, it just sticks around at the top of the screen (or wherever we put our messages). In a traditional server-rendered application this is less of a problem: Django messages are removed from the session when rendered, so once you reload the page (by navigating to a link, for example) the message goes away. In an HTMX-enhanced site however the whole point is to avoid reloading the page as much as possible, by just re-rendering parts of the page in response to server-side actions. But that means the messages aren't removed in between requests.
This is more of a client-side than server-side problem. If you remember earlier we included the Alpine.js CDN as well as HTMX, because we anticipated the need for doing small client-side interactions. Let's go back to our _messages.html
template and enhance it with Alpine.js attributes:
<div id="messages"
class="messages"
{% if hx_oob %}hx-swap-oob="true"{% endif %}>
{% if messages %}
<ul>
{% for message in messages %}
<li class="message message-{{ message.tags }}"
role="alert"
x-data="{show: true}"
x-show="show"
x-transition
x-init="setTimeout(() => show = false, 2000)">{{ message.message }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
The x-data
attribute sets a variable show
. The x-show
attribute will hide the element if show
is false
.
We use x-transition
to make this a little smoother (with some more CSS you can tweak the transition effects).
Finally, with x-init
we trigger the immediate behaviour when the <li>
element is rendered: it will set a timeout that will set the show
flag to false
after a couple seconds.
Now, when our messages are rendered, they will disappear automatically after a little while. Note that this functionality will work whether we are rendering the messages in our HTMX response, or after a full-page refresh or redirect.
Full code for this article can be found here.