name: django-htmx description: "Build modern dynamic web applications with Django and htmx - partial rendering, HTMX-specific responses, and seamless frontend integration" metadata: author: mte90 version: 1.0.0 tags: - django - htmx - python - web - frontend - partial-rendering - ajax
Django HTMX
Django-htmx provides seamless integration between Django and htmx for building modern, dynamic web applications without writing complex JavaScript.
Installation
pip install django-htmx
Add to INSTALLED_APPS:
INSTALLED_APPS = [
...
"django_htmx",
]
Add the middleware:
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django_htmx.middleware.HtmxMiddleware", # Add this
...
]
Core Concepts
Request Detection
The middleware adds request.htmx to detect htmx requests:
from django.shortcuts import render
def my_view(request):
if request.htmx:
template_name = "partial.html"
else:
template_name = "full.html"
return render(request, template_name)
HtmxDetails Attributes
The request.htmx object provides these attributes:
request.htmx- Boolean, True if request is from htmxrequest.htmx.boosted- True if request is from boosted element (hx-boost)request.htmx.current_url- Current URL in browser from HX-Current-URL headerrequest.htmx.current_url_abs_path- Absolute path form of current_urlrequest.htmx.history_restore_request- True if request is for history restorationrequest.htmx.target- Target element ID from HX-Target headerrequest.htmx.trigger- Trigger element ID from HX-Trigger headerrequest.htmx.trigger_name- Trigger element name from HX-Trigger-Name headerrequest.htmx.prompt- User response to hx-prompt attributerequest.htmx.triggering_event- Deserialized JSON from event-header extension
Template Tags
Load and use in templates:
{% load django_htmx %}
<!DOCTYPE html>
<html>
<head>
{% htmx_script %}
</head>
<body hx-headers='{"x-csrftoken": "{{ csrf_token }}"}'>
...
</body>
</html>
Django Templates
{% load django_htmx %}
<!doctype html>
<html>
<head>
{% htmx_script %}
</head>
<body hx-headers='{"x-csrftoken": "{{ csrf_token }}"}'>
...
</body>
</html>
Use minified=False for debugging:
{% htmx_script minified=False %}
Jinja2
from jinja2 import Environment
from django_htmx.jinja import htmx_script
def environment(**options):
env = Environment(**options)
env.globals.update({"htmx_script": htmx_script})
return env
{{ htmx_script() }}
HTTP Response Classes
HttpResponseClientRedirect
Triggers a client-side redirect (HX-Redirect header):
from django_htmx.http import HttpResponseClientRedirect
def sensitive_view(request):
if not sudo_mode.active(request):
next_url = request.htmx.current_url_abs_path or ""
return HttpResponseClientRedirect(f"/activate-sudo/?next={next_url}")
...
HttpResponseClientRefresh
Triggers a page reload (HX-Refresh header):
from django_htmx.http import HttpResponseClientRefresh
def partial_table_view(request):
if page_outdated(request):
return HttpResponseClientRefresh()
...
HttpResponseLocation
Makes htmx do a client-side "boosted" request (HX-Location header):
from django_htmx.http import HttpResponseLocation
def wait_for_completion(request, action_id):
...
if action.completed:
return HttpResponseLocation(f"/action/{action.id}/completed/")
...
HttpResponseStopPolling
Stops polling when using hx-trigger="every":
from django_htmx.http import HttpResponseStopPolling
def my_pollable_view(request):
if event_finished():
return HttpResponseStopPolling()
...
Or use the constant directly:
from django_htmx.http import HTMX_STOP_POLLING
from django.shortcuts import render
def my_pollable_view(request):
if event_finished():
return render(request, "event-finished.html", status=HTMX_STOP_POLLING)
...
Response Modifying Functions
push_url
Push a new URL to the browser history:
from django_htmx.http import push_url
def leaf(request, leaf_id):
...
if leaf is None:
response = branch(request, branch=leaf.branch)
return push_url(response, f"/branch/{leaf.branch.id}")
...
replace_url
Replace the current URL in browser history:
from django_htmx.http import replace_url
def dashboard(request):
...
response = render(request, "dashboard.html", ...)
return replace_url(response, "/dashboard/")
reswap
Override the swap method:
from django.shortcuts import render
from django_htmx.http import reswap
def employee_table_row(request):
...
response = render(...)
if employee.is_boss:
reswap(response, "afterbegin")
return response
retarget
Override the target element:
from django.shortcuts import render
from django.views.decorators.http import require_POST
from django_htmx.http import retarget
@require_POST
def add_widget(request):
...
if form.is_valid():
response = render(request, "widget-table.html", ...)
return retarget(response, "#widgets")
return render(request, "widget-table-row.html", ...)
reselect
Override the content selection:
from django_htmx.http import reselect
def update_table(request):
response = render(request, "table.html", ...)
return reselect(response, "tbody")
trigger_client_event
Trigger client-side events:
from django.shortcuts import render
from django_htmx.http import trigger_client_event
def end_of_long_process(request):
response = render(request, "end-of-long-process.html")
return trigger_client_event(
response,
"showConfetti",
{"colours": ["purple", "red", "pink"]},
after="swap", # "receive", "settle", or "swap"
)
Best Practices
Partial Rendering
Use django-template-partials for efficient partial rendering:
pip install django-template-partials
{% extends "_base.html" %}
{% load partials %}
{% block main %}
{% partialdef country-table inline %}
<table id="country-data">
...
</table>
{% endpartialdef %}
{% endblock main %}
In views:
def country_listing(request):
template_name = "countries.html"
if request.htmx:
template_name += "#country-table"
countries = Country.objects.all()
return render(request, template_name, {"countries": countries})
Swapping Base Template
def partial_rendering(request):
if request.htmx:
base_template = "_partial.html"
else:
base_template = "_base.html"
return render(request, "page.html", {"base_template": base_template})
{% extends base_template %}
{% block main %}
...
{% endblock %}
CSRF Protection
Always include CSRF token in htmx requests:
<body hx-headers='{"x-csrftoken": "{{ csrf_token }}"}'>
Caching with HTMX
Add appropriate Vary headers for cacheable responses:
from django.shortcuts import render
from django.views.decorators.cache import cache_control
from django.views.decorators.vary import vary_on_headers
@cache_control(max_age=300)
@vary_on_headers("HX-Request")
def my_view(request):
if request.htmx:
template_name = "partial.html"
else:
template_name = "complete.html"
return render(request, template_name, ...)
HTMX Extensions
Download extensions locally (avoid CDNs):
curl -L https://unpkg.com/htmx-ext-ws/dist/ws.min.js -o static/htmx-ext-ws.min.js
{% load django_htmx static %}
<!doctype html>
<html>
<head>
{% htmx_script %}
<script src="{% static 'htmx-ext-ws.min.js' %}" defer></script>
</head>
...
</html>
Type Checking
For type-checking, extend HttpRequest:
from django.http import HttpRequest as HttpRequestBase
from django_htmx.middleware import HtmxDetails
class HttpRequest(HttpRequestBase):
htmx: HtmxDetails
References
- Official Documentation: https://django-htmx.readthedocs.io/
- GitHub Repository: https://github.com/adamchainz/django-htmx
- htmx Reference: https://htmx.org/reference/