This article shows you how to use htmx in Django. What is htmx? According to the htmx website:
htmx allows you to access AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML, using attributes, so you can build modern user interfaces with the simplicity and power of hypertext.
The example that we’ll craft is an ability to delete a task from a list of tasks without reloading the whole page.
Step 1: Add The Script
Here’s a base template
for our example
that I’ll store in base.html
.
<html>
<head><title>Htmx Demo!</title></head>
<body>
{% block main %}{% endblock %}
<script src="https://unpkg.com/htmx.org@1.1.0"></script>
<script>
document.body.addEventListener('htmx:configRequest', (event) => {
event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
})
</script>
</body>
</html>
The critical element to this template (and the impetus for this entire article!) is the event listener after the script that includes htmx.
Because htmx uses HTTP methods other than GET, Django will expect a CSRF security token in the requests. The small function added in the script block ensures that htmx AJAX requests includes a CSRF token that allows non-GET requests to work.
If you don’t include this configuration,
Django will respond to requests
with a 403 Forbidden
status code.
Edit 2021-12-31:
As an alternative to the event listener,
you can now use hx-headers
to achieve the same effect.
Because htmx will look at parent elements
to find additional attibutes,
you can apply the CSRF token globally
to your site
by adding to the body
tag like this:
<html>
<head><title>Htmx Demo!</title></head>
<body
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
>
{% block main %}{% endblock %}
<script src="https://unpkg.com/htmx.org@1.1.0"></script>
</body>
</html>
Step 2: Create The Tasks List View
We need a view to display tasks.
Let’s assume that we have a Task
model
that includes a description
text field.
# application/views.py
from django.shortcuts import render
from .models import Task
def display_tasks(request):
tasks = Task.objects.all()
return render(request, 'display_tasks.html', {'tasks': tasks})
# project/urls.py
from django.urls import path
from application.views import display_tasks
urlpatterns = [
path('tasks/', display_tasks, name='display_tasks'),
]
That should be enough
for your Django project
to display the tasks at /tasks/
.
Let’s see the display_tasks.html
template.
{% extends "base.html" %}
{% block main %}
<h1>Tasks</h1>
{% include "tasks_list.html" %}
{% endblock %}
That template depends on tasks_lists.html
which looks like:
<div id="tasks">
{% for task in tasks %}
<div>{{ forloop.counter }} {{ task.description }}</div>
{% endfor %}
</div>
This version of the view doesn’t use htmx
or have the ability
to delete a task.
The view will display a numbered list
of items.
(Yes,
I recognize that this could have used an ol
tag,
but use your imagination
that the layout is much more complicated
than this example.)
Step 3: Add The Delete Task View
I included the for loop to illustrate a point. What happens if you delete a row via JavaScript and remove the div from the DOM? The numbering is going to be messed up since a number value will be missing!
Our htmx solution will solve that issue. Let’s see the view.
# application/views.py
from django.views.decorators.http import require_http_methods
...
@require_http_methods(['DELETE'])
def delete_task(request, id):
Task.objects.filter(id=id).delete()
tasks = Task.objects.all()
return render(request, 'tasks_list.html', {'tasks': tasks})
Observe that this DELETE view returns an HTML response
of the tasks_list.html
partial template.
This is crucial for htmx
because the library will use that content.
# project/urls.py
from django.urls import path
from application.views import delete_task, display_tasks
urlpatterns = [
path('tasks/', display_tasks, name='display_tasks'),
path('tasks/<int:id>/delete/', delete_task, name='delete_task'),
]
Now that we have the delete view, we can add the new functionality and use htmx.
<div id="tasks"
hx-confirm="Are you sure you want to delete this task?"
hx-target="#tasks"
hx-swap="outerHTML">
{% for task in tasks %}
<div>{{ forloop.counter }} {{ task.description }}
<a class="block mr-2 hover:text-gray-600"
hx-delete="{% url 'delete_task' task.id %}">
Delete?
</a>
</div>
{% endfor %}
</div>
Here’s how this flows:
- When a user clicks “Delete?,” that user will be prompted
with a standard browser alert box
with the question from
hx-confirm
. This works for each taskdiv
because htmx will inherit attributes from its parent content. Using attributes from parents can reduce the amount of extra markup in your output. - If the user confirms,
htmx will send a
DELETE
request to a URL like/tasks/42/delete/
because ofhx-delete
. - The delete view will delete the task from the database and return a new list (properly numbered!) in the response.
- Htmx will take the response
and set the content
in
hx-target
. Thehx-swap
instructs htmx how to place the new content. The default sets the inner HTML, but we want to replace thetasks
ID completely since the partial template includes a newtasks
ID.
This example is simplified on purpose so you can probably see ways to do this using POST and redirects, but that’s not quite the point I’m trying to make. Because htmx can swap in content, the tool opens all kinds of options to modify a page dynamically and drive that content using views and server side rendering.
I hope you see the value and simplicity that htmx provides from reviewing this example. I’m excited to use htmx in my projects to sprinkle in dynamic functionality while minimizing the amount of JavaScript that I have to write.
Learn More
Do you want to learn how Django works or what Django is used for? Then I suggest you check out my Understand Django series of articles next. In that series, I explain Django to new (and old!) web developers. I think it will help you on your journey to becoming a Django dev.
Subscribe to my newsletter and follow me on X to learn more about Django as I release new content.