Django things you want with HTMX
Works with Flask and FastAPI as well if you know what you're doing
Use HTTP 303 for redirections.
Setup django-htmx to see Django error response pages despite Ajax.
Trigger back client side events from the backend using the
Doesn't it work out of the box?
Yes, it does.
You can drop a script tag and start using HTMX with any Django website and call it a day. That's one of the benefits of the tech. Like all techs though, the more you use it, the more you get comfy with it, and so demand more of it.
So let's share.
Long ago, when XML was the future, I was hunting mammoths with notepad++, painting in caves with PHP or cooking with fastcgi; and we used forms to do pretty much every interaction with the user.
The typical workflow is the following:
Setup validation in the end point.
If it's a POST request and the data is valid, perform the user requested operation and redirect (to avoid F5 side effects).
If not, setup user feedback, and display the form in HTML.
Doing that with HTMX on top is a good way to start with something dumb and improve it by sparkling a bit of magical automatic AJAX. Later on, you'll likely want to trigger requests on more granular widgets and events, but as a starting point, it's great.
However, Django canonical way of sending back redirection responses is the redirect function, with a signature looking like this:
redirect(to, *args, permanent=False, **kwargs)
True depending on whether you want the HTTP status code to be 302 (Found) or 301 (Permanent redirect).
However, what you sometimes want with HTMX is the status code 303 (See other). Indeed, with 303 the client should use the same HTTP verb to follow the redirection than the original request. And while with a traditional client this didn't matter since you could only do POST and GET, with HTMX you can do POST, GET, PUT, DELETE and PATCH from anything. It suddenly is quite convenient to use 303.
Fortunately, you can create an HTTP response manually in Django. When I need this kind of redirection, I use the following snippet:
def see_other(to, *args, **kwargs): response = HttpResponse(content="See other", status=303) response["Location"] = resolve_url(to, *args, **kwargs) return response
This will issue a 303 redirection, and on top of that, use Django's resolver to build the
Location header so you can put an URL name instead of a full path.
Sometimes, I even put a query string builder in there.
Debug 500 like it's 1999
When Django is in debug mode, it will nicely display any error in the browser. This is useful to figure out things like routing issues, where fix + reload is way more efficient than popping a breakpoint() or watching the server's log.
But when you do an Ajax request, this helpful page gets lost in the browser web console behind several clicks.
For those situations, having a middleware and a bit of JS that prompts HTMX to swap error pages with the body and restore the original behavior is quite convenient.
Now, you could code it yourself, but frankly I just drop the django-htmx package utility for it and call it a day. This lib also has a shortcut to set automatically the CSRF token, which makes it worth having it in your toolbox anyway.
Sending back events
The first steps in HTMX land are usually timid. A few requests here, maybe a boost there, and tada, my old crappy page is now a modern crappy page.
But it will creep on you like the mushrooms from "The last of us", taking more and more control of your <body>.
You will start making smaller requests on smaller widgets. Then use click events (or even... polling!) on divs to utter PUT verbs like a mad men.
But reading again the HTMX doc, which is very small because the lib's public API fits on a post-it, you will notice a discreet yet intriguing section about responses.
"Wait, what?", you mumble to yourself, oblivious to the comments of your colleagues about your mental health that got more and more frequent since you started to spell out HATEOAS last Tuesday morning.
"You mean there is a backend part of HTMX as well?"
Well, yes, my dear fellow hatter, that's the point of HTMX, the client is thin, the backend is queen.
You can attach events in the response, which are basically arbitrary slugs like "update-notifications" or "clear-search-bar", and HTMX turn them into browser events on the other side. Which you can listen to with
hx-trigger, to make requests from the browser as a reaction to said events. Which will get more responses with more events...
It's beautiful. Suddenly you realize you can push a button, get its new state from the server, which returns an even telling the page to update the main content, which gets a response with an event that tells a counter to be set to zero, and so on.
The whole page becomes a set of reactive little servants controlled by the queen.
To send events, you have to set the
HX-Trigger header. In Django it’s basically setting a key on the response.
django-htmx provides a helper for that, but I usually settle for a more basic one that requires less to type:
def render_with_events(request, template_name, context=None, events=()): if isinstance(events, str): events = [events] response = render(request, template_name, context) if events: response["HX-Trigger"] = json.dumps(dict.fromkeys(events)) return response
There are plenty more goodies you can use, like partial rendering or out of band swapping, to squeeze the most juice out of your HTTP back and forth. But honestly, I haven't got to the point I need them.
Sure, it's nice to render less templates and not trigger as many HTTP responses (and hence endpoint pipelines), but machines and network are so good nowadays you need a lot to arrive at the level where you actually benefit from optimizations like this.
Not to mention good caching is a lower and much sweeter hanging fruit.
response["HX-Trigger"] = “update-user-unread-articles” ?