Using the Django messages framework with HTMX

This article explains how to create a middleware that extracts the messages from Django’s messages framework to make them available to HTMX. It’s a condensed version of my 30-minute video: Django+HTMX: integration with the messages framework and a follow-up to my previous article on Django+HTMX, but you can read it independently.

Using the Django messages framework with HTMX

This article is designed as a tutorial that shows how to recreate the sample project. If this format doesn’t suit you, please watch the video as it takes a radically different angle.

I’m not too fond of long articles, so I’ll go straight to the point, even if I have to simplify some aspects. After reading this article, please look at the complete project in the GitHub repository.

Overview

Starting from scratch, we’ll create a minimalistic Django application with only a home page. This page will contain one button. Clicking on this button will invoke a Django view that adds a message into the messages framework. A custom middleware will extract the messages to put them in the HX-Trigger header. HTMX recognizes this header and raises a JavaScript event. A custom JavaScript file will listen to this event and create a Bootstrap 5 toast for each message.

You can follow the steps of this article in the commits of the blog branch of the Git repository.

Step 1: Create the project

Let’s start an empty Django project:

$ django-admin startproject demoproject .
$ python manage.py startapp demoapp

Edit demoproject/settings.py to add demoapp to the INSTALLED_APPS:

INSTALLED_APPS = [
  ...
  'demoapp',
]

Step 2: Create the home view

Create demoapp/templates/home.html from Bootstrap’s Quick Start example:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Bootstrap demo</title>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" crossorigin="anonymous">
  </head>
  <body>
    <h1>Hello, world!</h1>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-u1OknCvxWvY5kfmNBILK2hRnQC3Pr17a+RTT6rIHI7NnikvbZlHgTPOOmMi466C8" crossorigin="anonymous"></script>
  </body>
</html>

In demoapp/views.py, create a view that renders this template:

def home(request):
    return render(request, "home.html")

In demoproject/urls.py, attach this view to the root path:

from demoapp import views

urlpatterns = [
    path("", views.home),
]

Start the development server to make sure everything is correct so far:

$ python manage.py runserver

You can ignore the warning about the unapplied migrations since we won’t use models in this article.

Open http://127.0.0.1:8000/ on your browser. You should see a blank page with just “Hello, world!”.

Step 3: Install HTMX

Copy the <script> tag from HTMX’s Quick Start and paste it before </body> in home.html

<html lang="en">
  <body>
    ...
    <script src="https://unpkg.com/[email protected]"></script>
  </body>
</html>

Step 4: Add the button

Add hx-get="/message" so that HTMX will perform a GET request every time someone clicks on this button:

<button class="btn btn-primary" hx-get="/message">Emit message</button>

In an actual project, you should use the url template tag instead of hard-coding the URL.

Open your browser’s developer console. Click on the button, and you should see an error like this:

GET http://127.0.0.1:8000/message 404 (Not Found)

Indeed, we didn’t create the /message view, so Django returns a 404.

Step 5: Create the /message view

Import Django’s messages framework and HttpResponse in views.py:

from django.contrib import messages
from django.http.response import HttpResponse

Add the following function in views.py:

def message(request):
    messages.success(request, "It works!")
    return HttpResponse(status=204)

As you can see, this view adds a “success” message. Then, it returns an empty response, but you could return a regular response.

Add the following route to urls.py:

urlpatterns = [
    ...
    path("message", views.message),
]

Refresh the page on your browser and click on the button. The Console tab should not show any errors, and the Network tab should show the request returning status 204

Step 6: Create the middleware

Create demoproject/middleware.py with the following content:

import json
from django.utils.deprecation import MiddlewareMixin
from django.contrib.messages import get_messages

class HtmxMessageMiddleware(MiddlewareMixin):

    def process_response(self, request, response):

        if "HX-Request" in request.headers:

          response.headers["HX-Trigger"] = json.dumps({
              "messages": [
                {"message": message.message, "tags": message.tags}
                for message in get_messages(request)
              ]
          })

        return response

⚠️ This is a simplified version. Use the full version in actual projects.

For each request made with HTMX (detected with the HX-Request header), this middleware puts all the messages into the HX-Trigger header of the response. In our case, this header will contain:

{"messages":[{"message":"It works!","tags":"success"}]}

When it sees this header in a response, HTMX raises a JavaScript event for each of the keys in the JSON object. In our case, it will raise a CustomEvent named "messages" carrying the following payload:

[{"message":"It works!","tags":"success"}]

We’ll come back to this event in a moment, but first, we must enable this middleware.

Open settings.py and append our middleware’s path to the MIDDLEWARE option:

MIDDLEWARE = [
    ...
    'demoproject.middleware.HtmxMessageMiddleware',
]

Go back to your browser and click the “Emit message” button. Look a the last request on the Network tab, and you should see a response like this:

HTTP/1.1 204 No Content
Date: Mon, 19 Sep 2022 18:29:02 GMT
Server: WSGIServer/0.2 CPython/3.9.13
Content-Type: text/html; charset=utf-8
HX-Trigger: {"messages": [{"message": "It works!", "tags": "success"}]}
Content-Length: 0
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

Notice the HX-Trigger header containing our message.

Step 7: Receive the messages in JavaScript

Create demoapp/static/toasts.js with the following content:

htmx.on("messages", (event) => {
  console.log(event.detail.value)
})

htmx.on() adds a listener for the specified event (you can see it as a shorthand for document.addEventListener()). In our case, we listen to the “messages” event to match the name we used in the HX-Trigger header. For now, we use a simple event handler that only prints the event’s payload.

Add the required <script> before </body>, like so:

{% load static %}
<html lang="en">
  <body>
    ...
    <script src="{% static 'toasts.js' %}"></script>
  </body>
</html>

Notice that I added {% load static %} at the top of the file so we can use the static template tag.

Restart the server so that Django sees this new static/ folder. If you don’t restart the server, you’ll get a 404 on toasts.js.

Go back to your browser, refresh the page a click on the button. You should see something like this in the Console tab:

[
    {
        "message": "It works!!",
        "tags": "success"
    }
]

Step 8: Add an HTML toast template

Open home.html and add the following block before the <script> tags:

<div data-toast-container class="toast-container position-fixed top-0 end-0 p-3">
  <div data-toast-template class="toast align-items-center border-0" role="alert" aria-live="assertive" aria-atomic="true">
    <div class="d-flex">
      <div data-toast-body class="toast-body"></div>
      <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
    </div>
  </div>
</div>

This markup is inspired by Bootstrap’s example for Toasts and must be changed if you use a different CSS framework.

This toast is not visible. We’ll use it as a template to create more toasts. Notice the data attributes data-toast-container, data-toast-template, and data-toast-body, which we’ll use to find the elements.

Step 9: Create a toast for each message

Add the following function in toasts.js:

function createToast(message) {
  // Clone the template
  const element = htmx.find("[data-toast-template]").cloneNode(true)

  // Remove the data-toast-template attribute
  delete element.dataset.toastTemplate

  // Set the CSS class
  element.className += " " + message.tags

  // Set the text
  htmx.find(element, "[data-toast-body]").innerText = message.message

  // Add the new element to the container
  htmx.find("[data-toast-container]").appendChild(element)

  // Show the toast using Bootstrap's API
  const toast = new bootstrap.Toast(element, { delay: 2000 })
  toast.show()
}

This function takes an object like {message: "It works!", tags: "success"} and creates a toast element in the DOM. This code is specific to Bootstrap 5 and must be adapted to your CSS framework.

Now, let’s call this function for each incoming message:

htmx.on("messages", (e) => {
  e.detail.value.forEach(createToast)
})

Refresh the page in your browser and click on the button. Each click should create a white toast with the message “It works!”.

We can fix the color of this toast by configuring the tags in settings.py:

from django.contrib import messages

MESSAGE_TAGS = {
    messages.DEBUG: "bg-light",
    messages.INFO: "text-white bg-primary",
    messages.SUCCESS: "text-white bg-success",
    messages.WARNING: "text-dark bg-warning",
    messages.ERROR: "text-white bg-danger",
}

Go back to your browser and click on the button again. The new toast should now have the expected green background.

Conclusion

I went as fast as possible to present the technique, so I had to cut some corners. I invite you to consult the GitHub repository for the complete source code.

Here is how the code in the repository differs from this article:

  1. It bundles message-related code in a htmx_messages app containing:
    • toasts.js
    • toasts.html
    • middleware.py
  2. It supports classic Django messages thanks to:
    • {% for message in messages %} in toasts.html
    • Code to show existing toasts in toasts.js
  3. It supports views that set the HX-Trigger header:
    • The middleware reads and patches the header accordingly
    • It supports both the short and the extended syntaxes of HX-Trigger
  4. toasts.js uses an IIFE so that it can be concatenated with other JavaScript files.
  5. The message view generates random toast messages with different levels.

You can see all these differences explained in the video as well.

If you want to see an alternative technique, check out my next article.