Primer to Django and htmx

When building a web app with Django and its built-in template language, I would say that it is good enough for 80% of the use cases. But there still remains, this small percentage that needs to be a bit more dynamic and the built-in template language is not enough.

The usual response to this issue is to use a full-featured front-end framework. But this adds a tremendous level of complexity to the web app. And while there are some kinds of web apps that require this level of functionality, if you only need a bit of dynamic functionality there's an easier and less complex way to do this.

One of the most popular ways of adding a bit of dynamic functionality to your web app is using htmx. This is a Javascript framework, "disguised" as an HTML extension. You could say that it adds "superpowers" to regular HTML. In short, it allows any element to make any kind of HTTP request (GET, POST, PUT, etc) without the need for a <form> element. It does quite a lot more, but this is the one-line description.

How it works

It's pretty simple: it allows for the HTML response to replace a specific part of the page, rather than the complete page. When you are writing a Django view, what you are returning from that view replaces the entire page. With htmx, you can specify which exact part of the page to be replaced. This way you can have a bit of dynamic behavior, without reloading the entire page.

Setup

Installing htmx is extremely easy: you just include the JS file in your <head> section. See the official documentation for the latest version.

<head>
    <script src="https://unpkg.com/htmx.org@1.8.0"></script>
    [...]
</head>
base.html

While this is the easiest and quickest way to get you started, what you should consider is hosting a copy of the script. Why this is better than using the CDN version, is beyond the scope of this post, but there are valid reasons for doing so.

In addition, you should consider adding the following attribute to your HTML base template. Django views for accepting POST requests, require the CSRF token as a security measure. Doing this ensures that this token is included in every POST request you make from your site.

<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>

Practical example

A common example would be the implementation of an active search page. Without the use of htmx, every search would require a complete page reload to show the results for the new keyword.

View

class SearchView(View):

    def get(self, request):
        search_term = request.GET.get('search', None)
        if search_term:
            items = \
                Item.objects.filter(name__contains=search_term).all()
            template = 'search_results.html'
        else:
            items = Item.objects.all()
            template = 'search.html'
        return render(request=request, 
                      template_name=template,
                      context={
                          'items': items,
                      })
search_view.py

To determine whether this should return the entire page or just the search results section, we check the search GET parameter. If such a term exists, then we filter the items to include only the ones that have the entered keywords. Otherwise, all items are returned. Finally, we either render the complete page or just the results section by selecting the appropriate template (see the following subsection).

Templates

<input class="input" 
       name="search" 
       type="search"
       hx-get="{% url 'search' %}"
       hx-trigger="keyup changed delay:500ms, search"
       hx-target="#search-results"
       hx-swap="innerHTML"/>

<div id="search-results">
    {% include "search_results.html" %}
</div>
search.html
{% for item in items %}
    <div>
        {{ item.name }}
    </div>
{% endfor %}
search_results.html

Inside templates are where all the htmx "magic" happens.

The hx-get specifies the URL to which the GET request will be made. Note here that because this is an <input> element the value of the text box will be included as a GET parameter in the request with the name search. The hx-trigger specifies when this GET request will be made. The hx-target and hx-swap specify how the section of the web page will be replaced by the result of the GET request.

Combine all of this, and you have a Django-powered dynamic search page, where as soon as you type something in the search bar, the results will be updated without a full-page reload.

There are many many more things you can do with htmx, so I urge you to have a look at the official project website.

Happy coding!