I have followed the progress of HTMX and love it. It is a small amount of javascript (only 14kb gzipped) imported in the header tag:

<script src="https://unpkg.com/htmx.org@1.9.2"></script>

The creator, Carlton Gross, developed HTMX as a successor to intercoolerjs, and named it to be an extension to HTML. It allows GET/POST/PUT/DELETE calls over AJAX directly from the HTML tag to the server, and receives HTML in response which can be targetted by CSS attributes to specific parts of the DOM.

There are 4 key HTMX attributes in the average call:

hx-get / hx-post // the URL endpoint
hx-target // the CSS id of the element to replace
hx-swap // accepts 'innerHTML', 'outerHTML', 'afterbegin', 'beforebegin', 'afterend', 'beforeend', 'delete', 'none'
hx-trigger // an event type to trigger the request: usually click, key up, change. It also accepts attributes such as delay.

In this way, for example, a refresh button onclick can send a get request to the backend to requery the database serverside, and then return an html list of items to render, rather than the whole template. Suddenly a django app becomes reactive. Instead of the user having to fill in the whole form then clicking submit for example, now it is possible to update the model with every key press.

HTMX is agnostic of what language the backend is written in, and so called ‘async transparent’, meaning no promises, no async await and so on. In fact, it means, at least for a new project, you can have a truly server side rendered app without a whole load of javascript on the front end to recreate the useability that these days people demand. In fact, no more package.json, all the configs, node_module bloat and dependency uncertainty that comes with javascript frameworks like React.

It seems almost as though with Django you can now have your cake and eat it. You get to keep all the templating and Django magic that comes with 20+ years of open source support, without wrangling with a frontend framework as well.

Use case

Epilepsy12 is a national audit of epilepsy care delivered by hospitals across England and Wales. Clinicians submit data on all children under their care diagnosed with epilepsy over the last year, to allow the audit teams at RCPCH to compare standards and care processes across all UK centres. There is more on Epilepsy12 here.

In Epilepsy12 we have made extensive use of HTMX. It was a specific requirement of E12 that users be able to complete each question in the audit in any order, that progress never be lost, and progress be continuously updated and reported back to the user. The idea that a user fills out a bunch of fields in a form, then clicks submit and waits for Django to collect up all the answers, validate them and then persist in the model was not going to work. With HTMX though, it was possible for each field to post its results directly to the view for validation and saving, and receive back an updated snippet of HTML, for example to show the field with a pink tick to show that measure had been scored.

  1. Define the route for the POST request:

urls.py

path(
    "management/<int:management_id>/individualised_care_plan_includes_general_participation_risk",
    views.individualised_care_plan_includes_general_participation_risk,
    name="individualised_care_plan_includes_general_participation_risk",
),
  1. Add a tag to the template element in the on click event:

template.py

<div class="ui rcpch_dark_blue buttons">
    <div
        class="ui mini compact rcpch_dark_blue button"
        hx-target="#individualised_care_plan"
        hx-post="{% url for 'individualised_care_plan_includes_general_participation_risk' %}"
        hx-swap="innerHTML"
        hx-trigger='click'
        name="button-true"
    >
        Yes
    </div>
    <div
        class="ui mini compact rcpch_dark_blue button"
        hx-target="#individualised_care_plan"
        hx-post="{% url for 'individualised_care_plan_includes_general_participation_risk' %}"
        hx-swap="innerHTML"
        hx-trigger='click'
        name="button-false"
    >
        No
        <i class="htmx-indicator spinner loading icon" id="spinner"></i>
    </div>
</div>
  1. Collect the name of the element in the view and update the model

views.py

@login_required
@user_may_view_this_child()
@permission_required("epilepsy12.change_management", raise_exception=True)
def individualised_care_plan_includes_general_participation_risk(
    request, management_id
):
    """
    This is an HTMX callback from the individualised_care_plan partial template
    It is triggered by a toggle in the partial generating a post request
    This inverts the boolean field value, and returns the same partial.
    """

    if request.htmx.trigger_name == "button-true":
        field_value = True
    elif request.htmx.trigger_name == "button-false":
        field_value = False
    else:
        # an error has occurred
        print("Error has occurred")

    management = Management.objects.get(pk=management_id)

    management.individualised_care_plan_includes_general_participation_risk = field_value
    management.save()

    context = {"management": management}

    template_name = "epilepsy12/partials/management/individualised_care_plan.html"

    response = render(request=request, template_name=template, context=context)

    return response

A partial template is returned, updating only that part of the screen which has changed.

Custom triggers

The above example works well when there is a simple feedback loop between the child partial and the view which updates some aspect of state and returns the same partial with the update. It is quite a common use case though for an event on one part of the screen to trigger a refresh on another part of the screen.

HTMX can handle this in one of 2 ways - custom triggers or what it calls ‘out of band’ swaps. My favourite of these is custom triggers and we have used these extensively in Epilepsy12.

*epilepsy12 - assessment form alt assessmentscreenshot

Any field in the form triggers a recalculation of the scores in the form - which fields are now complete, which are left still to complete - and these results need to be updated in the progress wheel in the steps on the left side of the screen. In addition, any forms that are complete, need to be flagged as such to the user, giving them permission to move on to the next form.

This is done by adding a custom htmx-trigger to the header in the response.

from django_htmx.http import trigger_client_event

... at the end of the view function before the response is returned ...
# trigger a GET request from the steps template
    trigger_client_event(
        response=response, name="registration_active", params={}
    )  # reloads the form to show the active steps

This in turn calls the registration_active endpoint:

# HTMX generic partials
def registration_active(request, case_id, active_template):
    """
    Call back from GET request in steps partial template
    Triggered also on registration in the audit
    """
    registration = Registration.objects.get(case=case_id)
    audit_progress = registration.audit_progress
    site = Site.objects.filter(
        site_is_actively_involved_in_epilepsy_care=True,
        site_is_primary_centre_of_epilepsy_care=True,
        case=registration.case,
    ).get()
    organisation_id = site.organisation.pk

    # enable the steps if has just registered
    if audit_progress.registration_complete:
        if active_template == "none":
            active_template = "register"

    context = {
        "audit_progress": audit_progress,
        "active_template": active_template,
        "case_id": case_id,
        "organisation_id": organisation_id,
    }

    return render(
        request=request, template_name="epilepsy12/steps.html", context=context
    )

This function returns the steps.html template, rendering the menu on the left side of the screen. In this way, when a button is clicked in the form, the model is updated with the new selection and the form button updated to show it has been selected. At the same time, the custom trigger registration_active is called, precipitating a refresh of the steps menu.