Django 5.2 simple_block_tag with HTMX
Django 5.2 is nearing its release date, and with it comes a new template tag, simple_block_tag()
.
From the release notes, it’s described as:
The new
simple_block_tag()
decorator enables the creation of simple block tags, which can accept and use a section of the template.
When compared with the complete example in the tag’s documentation, that description definitely buries the lede1.
Seeing the test.html example, I get very excited about the possibilities. There are several libraries out there designed to make the Django Template Language (DTL) support components2, but I haven’t been able to pick one yet. With this tag, I may not have to.
I can create my own components only using Django which are precisely what I need.
If that’s not Django at its best, I’m not sure what is.
Integrating simple_block_tag()
with HTMX
I’d like to use simple_block_tag()
3 to validate that the htmx attributes being specified are correct. Beyond that, I think it’s possible to use this tag to validate that the correct values are being specified during tests without having to write an integration test with Playwright or Selenium.
This tag may look like:
from django import template
register = template.Library()
KNOWN_HTMX_ATTRS = {
"hx-swap",
"hx-target",
# ...
}
class InvalidHTMXAttribute(Exception):
pass
def _htmx_validate_attributes(**kwargs):
"""
Perform extra validation on the htmx attributes.
Required to make testing a bit easier too.
"""
pass
@register.simple_block_tag()
def htmx(content):
attrs = dict(line.strip().split('=', maxsplit=1) for line in content.strip().split('\n'))
if unknown := (attrs.keys() - KNOWN_HTMX_ATTRS):
human_readable = ", ".join(list(sorted(unknown)))
raise InvalidHTMXAttribute(f"Unexpected HTMX attribute(s) used: {human_readable}")
_htmx_validate_attributes(**attrs)
return content
The template where this gets used could be:
{% extends "base.html" %}
{% load testapptags %}
{% block content %}
<button id="do-something-cool"
class="btn-class-or-tailwind"
{% htmx %}
hx-post="{% url 'my-cool-view' %}"
hx-target="#where-the-coolness-goes"
hx-swap="outerHTML"
{% endhtmx %}
type="button"
>
Click here to do something cool!
</button>
{% endblock %}
With this, we know if we’re specifying all of our HTMX attributes properly. That’s a nice improvement. But that still doesn’t help us confirm that the values specified are correct. We could have the htmx
tag validate the values of known attributes, such as hx-swap
where the value can only be one of a few preset options. This would go in _htmx_validate_attributes
. Maybe something like:
# https://htmx.org/attributes/hx-swap/
HTMX_SWAP_VALUES = {"innerHTML", "outerHTML", "..."}
def _htmx_validate_attributes(**kwargs):
if (swap := kwargs.get("hx-swap")) and swap is not None:
# Strip the quotes
swap = swap.strip("'\"")
if swap not in HTMX_SWAP_VALUES:
raise InvalidHTMXSwapValue(f"Invalid hx-swap value specified: {swap}")
return
Let’s write a test where we confirm that a specific usage of htmx
was called with our expected values. And yes, this will involve mock, sorry testing purists.
import pytest
# Assume we have pytest-mock installed too
@pytest.fixture
def spy_htmx(mocker):
import testapptags
spy_htmx = mocker.spy(testapptags, "_htmx_validate_attributes")
yield spy_htmx
@pytest.mark.django_db
def test_do_something_cool(client, spy_htmx):
response = client.get('/my-view/')
assert response.status_code == 200
spy_htmx.assert_called_once_with(
**{
"hx-post": '"/my-cool-view/"',
"hx-target": '"#where-the-coolness-goes"',
"hx-swap": '"outerHTML"',
}
)
Perhaps this seems a bit trivial. However, if you had hx-target="#item-{{ item.uuid }}
, this type of evaluation is much more helpful. Now you can confirm that the prefix is there and that a specific item was used in the value of hx-target
.
This doesn’t entirely replace an integration test, but it does help the testing story of HTMX + Django. It’s easier to build smaller, faster tests that can catch simple mistakes until Django LSPs4 become commonplace, removing the benefits here.
Hopefully, this gets you excited about Django 5.2! If it does, you can help Django by testing your application against the alpha or release candidate today! pip install 'Django~=5.2.0a1
and run your test suite. The broader Django community thanks you for your help!
If you have questions, thoughts or want to share your excitement, you can find me on the Fediverse, Django Discord server or via email.
-
TIL: https://www.merriam-webster.com/wordplay/bury-the-lede-versus-lead ↩
-
django-components, django-cotton, django-bird and probably others. ↩
-
I have typed this as simply_block_tag about 10 times now. If one slips through, I’m going to be very disappointed. ↩
-
See django-language-server and django-template-lsp. ↩