Django and HTMX Trigger Modifiers & CSS Transitions

This post will demonstrate how to use HTMX trigger modifiers, and how to use CSS transitions in the context of an HTMX request/response.

We will extend the code from the previous tutorial in this video. This code can be found here on Github.

The associated video for this post is below:

Objectives

In this post, we will:

  • Learn what HTMX trigger modifiers are, and how to use them.
  • Learn how to use CSS transitions in the context of HTMX requests/responses

Trigger Modifiers

In the first part of this tutorial, we will look at HTMX trigger modifiers.

Triggers correspond to events that occur on the webpage. Common triggers include: click, submit, change, keyup, keydown, mouseenter and mouseleave.

We will extend the code from the previous tutorial. There, we defined an hx-trigger="keyup" trigger on the username field of the registration form, meaning that an AJAX request to the backend was sent every time the user typed a new character in the form field.

One problem with this method is the volume of requests that get sent to your backend. Imagine 10,000 users, all typing into the username field. This would generate an AJAX request for each character, and could potentially overwhelm your server and degrade your application's performance.

Trigger modifiers can help here. They allow you to customize the behaviour of the trigger. Some common modifiers are listed below.

  • once - ensures the request is only sent one time
  • delay:<time> - delays sending the request until the time-interval expires without any new triggers occurring. If a new trigger occurs before the time-interval expires, then the time-interval is reset.
  • throttle:<time> - ensures that the request is sent after the time-interval. The element triggers at the end of the time-interval, and any new events that occurred between the original event and the end of the time-interval are ignored.
  • changed - the request will only be send if the value of the field has changed.

We are going to delay the sending of AJAX requests on our username field until an interval of 2-seconds has passed without any new keyup actions. This will prevent AJAX requests being sent while the user is still typing their desired username, and will instead only send the request 2-seconds after the final character has been typed. We will modify our form-field in templates/regisration/register.html - specifically, we will modify the hx-trigger attribute.

{% render_field form.username class="form-control" hx-post="/check_username/" hx-swap="outerhtml" hx-trigger="keyup delay:2s" hx-target="#username-error" %}

Notice that hx-trigger="keyup delay:2s" is now the value of the hx-trigger attribute. So, when a user types, no request is sent for 2-seconds - and if the user types another character, then the request is further delayed and the timer is reset. The request is only sent when the 2-second duration has expired without any other keyup events.

This differs from the hx-trigger="keyup throttle:2s" attribute. Throttling means that, when the keyup event occurs, the request will definitely be sent after 2-seconds. Any events that occur after this original event are discarded before the request is sent. This ensures regular delivery of requests, but still gives some protection against overwhelming the server with AJAX requests.

The delay and throttle modifers are, in my experience, the most useful modifiers. These are the ones you are likely to reach for.

CSS Transitions

HTMX enables easy use of CSS transitions, too. Let's walk through how to achieve this.

Underneath our form-field, we had a <div id='username-error'> element where our message from the backend was displayed. We also had the following view which returned HTML on the keyup trigger.

def check_username(request):
    username = request.POST.get('username')
    if get_user_model().objects.filter(username=username).exists():
        return HttpResponse("This username already exists")
    else:
        return HttpResponse("This username is available")

We are now going to modify this to return a <div id='username-error'> element that can be swapped in, and we are going to attach a class of success if the username is available, and error if the username is not available. It's vital that we keep the ID the same, so the transition/animation can occur. Replace the above code with the following.

def check_username(request):
    username = request.POST.get('username')
    if get_user_model().objects.filter(username=username).exists():
        return HttpResponse(
            "<div id='username-error' class='error'>This username already exists</div>"
        )
    else:
        return HttpResponse(
            "<div id='username-error' class='success'>This username is available</div>"
        )

We are now going to create CSS styles that enable the transition to occur. In the static/css/styles.css file, add the following styles.

.success {
    color: green;
    transition: all ease-in .5s;
}

.error {
    color: red;
    font-size: 50px;
    transform: rotate(45deg);
    transition: all ease-in 5s;
}

This code results in a transition occurring when the class changes from error to success, or vice-versa. When the class changes to error, a 5-second animation occurs where:

  • The text-colour transitions from green to red
  • The font-size transitions to 50px
  • The text rotates 45 degrees

So, we simply have to keep the same element ID, and in our CSS files we can specify the transition.

For this to work, we need to perform a few extra setup steps. Firstly, add the following CSS link to the base.html, along with the static template-tag.

{% load static %}
<link rel="stylesheet" href="{% static 'css/styles.css' %}" />

We also need to tell Django where to find our static files. Within settings.py add the following line.

STATICFILES_DIRS = [BASE_DIR / 'static']

Now, we're good to go. We should see a transition such as the following on the registration form.