Using HTMX with Django 4.2
django htmx python
HTMX + Django continues to be a compelling stack. With Django 4.2 LTS, there are new patterns and improvements. Here’s the updated guide.
Setup for Django 4.2
pip install django django-htmx
# settings.py
INSTALLED_APPS = [
...
'django_htmx',
]
MIDDLEWARE = [
...
'django_htmx.middleware.HtmxMiddleware',
]
<!-- base.html -->
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/htmx.org@1.9.6"></script>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
Core Patterns (Refreshed)
Partial Templates
The key pattern: return full page for normal requests, partial for HTMX:
# views.py
def article_list(request):
articles = Article.objects.all()
if request.htmx:
template = 'articles/partials/list.html'
else:
template = 'articles/list.html'
return render(request, template, {'articles': articles})
Or use a reusable decorator:
from functools import wraps
def htmx_partial(partial_template):
def decorator(view_func):
@wraps(view_func)
def wrapper(request, *args, **kwargs):
response = view_func(request, *args, **kwargs)
if request.htmx and hasattr(response, 'template_name'):
response.template_name = partial_template
return response
return wrapper
return decorator
@htmx_partial('articles/partials/list.html')
def article_list(request):
articles = Article.objects.all()
return render(request, 'articles/list.html', {'articles': articles})
Infinite Scroll
<!-- templates/articles/list.html -->
<div id="article-list">
{% include 'articles/partials/items.html' %}
</div>
<!-- templates/articles/partials/items.html -->
{% for article in articles %}
<article class="article">
<h2>{{ article.title }}</h2>
<p>{{ article.summary }}</p>
</article>
{% endfor %}
{% if has_more %}
<div hx-get="{% url 'articles:list' %}?page={{ next_page }}"
hx-trigger="revealed"
hx-swap="outerHTML"
hx-indicator="#loading">
<span id="loading" class="htmx-indicator">Loading...</span>
</div>
{% endif %}
# views.py
from django.core.paginator import Paginator
def article_list(request):
page = request.GET.get('page', 1)
paginator = Paginator(Article.objects.all(), 10)
page_obj = paginator.get_page(page)
context = {
'articles': page_obj,
'has_more': page_obj.has_next(),
'next_page': page_obj.next_page_number() if page_obj.has_next() else None,
}
template = 'articles/partials/items.html' if request.htmx else 'articles/list.html'
return render(request, template, context)
Form Handling
<!-- Modal form with HTMX -->
<button hx-get="{% url 'articles:create' %}"
hx-target="#modal"
hx-swap="innerHTML">
New Article
</button>
<div id="modal"></div>
<!-- templates/articles/partials/form.html -->
<div class="modal-backdrop" onclick="closeModal()">
<div class="modal-content" onclick="event.stopPropagation()">
<h2>{% if form.instance.pk %}Edit{% else %}New{% endif %} Article</h2>
<form hx-post="{% if form.instance.pk %}{% url 'articles:update' form.instance.pk %}{% else %}{% url 'articles:create' %}{% endif %}"
hx-target="#article-list"
hx-swap="innerHTML">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save</button>
<button type="button" onclick="closeModal()">Cancel</button>
</form>
</div>
</div>
<script>
function closeModal() {
document.getElementById('modal').innerHTML = '';
}
</script>
Form Validation
# views.py
def article_create(request):
if request.method == 'POST':
form = ArticleForm(request.POST)
if form.is_valid():
article = form.save()
# Return updated list on success
return render(request, 'articles/partials/items.html', {
'articles': Article.objects.all()[:10]
})
else:
form = ArticleForm()
# Return form (with errors on POST with invalid data)
return render(request, 'articles/partials/form.html', {'form': form})
Real-time Field Validation
<input type="text" name="username"
hx-post="{% url 'accounts:validate_username' %}"
hx-trigger="blur changed delay:300ms"
hx-target="#username-feedback"
hx-swap="innerHTML">
<span id="username-feedback"></span>
def validate_username(request):
username = request.POST.get('username', '')
if not username:
return HttpResponse('')
if len(username) < 3:
return HttpResponse('<span class="error">Too short</span>')
if User.objects.filter(username=username).exists():
return HttpResponse('<span class="error">Taken</span>')
return HttpResponse('<span class="success">Available</span>')
Django 4.2 Specific
With Async Views
from django.http import HttpResponse
from asgiref.sync import sync_to_async
async def async_article_list(request):
articles = await sync_to_async(list)(
Article.objects.all()[:10]
)
template = 'articles/partials/items.html' if request.htmx else 'articles/list.html'
return await sync_to_async(render)(request, template, {'articles': articles})
With Transaction Hooks
from django.db import transaction
def delete_article(request, pk):
article = get_object_or_404(Article, pk=pk)
with transaction.atomic():
article.delete()
# Clear cache after successful delete
transaction.on_commit(lambda: cache.delete('article_list'))
if request.htmx:
return HttpResponse('') # Remove element from DOM
return redirect('articles:list')
HTMX Extensions
Loading States
<button hx-post="{% url 'slow_action' %}"
hx-disabled-elt="this"
hx-indicator="#spinner">
Save
<span id="spinner" class="htmx-indicator">⏳</span>
</button>
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator {
display: inline;
}
.htmx-request.htmx-disabled-elt {
opacity: 0.5;
cursor: not-allowed;
}
Confirm Dialogs
<button hx-delete="{% url 'articles:delete' article.id %}"
hx-confirm="Delete '{{ article.title }}'?"
hx-target="#article-{{ article.id }}"
hx-swap="outerHTML swap:200ms">
Delete
</button>
Out-of-Band Updates
Update multiple elements with one response:
def update_article(request, pk):
article = get_object_or_404(Article, pk=pk)
form = ArticleForm(request.POST, instance=article)
if form.is_valid():
article = form.save()
# Main response
response = render(request, 'articles/partials/row.html', {'article': article})
# Add out-of-band update for notification area
notification = render_to_string(
'partials/notification.html',
{'message': 'Article updated!'}
)
response.content += notification.encode()
return response
<!-- partials/notification.html -->
<div id="notifications" hx-swap-oob="innerHTML">
<div class="alert success">{{ message }}</div>
</div>
Project Structure
myproject/
├── templates/
│ ├── base.html
│ ├── articles/
│ │ ├── list.html # Full page
│ │ ├── detail.html # Full page
│ │ └── partials/
│ │ ├── items.html # List items only
│ │ ├── row.html # Single row
│ │ └── form.html # Form modal
│ └── partials/
│ └── notification.html # Shared notification
Best Practices
1. Consistent URL Pattern
# urls.py
urlpatterns = [
path('articles/', views.article_list, name='list'),
path('articles/<int:pk>/', views.article_detail, name='detail'),
path('articles/create/', views.article_create, name='create'),
path('articles/<int:pk>/update/', views.article_update, name='update'),
path('articles/<int:pk>/delete/', views.article_delete, name='delete'),
]
2. Reusable Partials
<!-- Include with context -->
{% include 'articles/partials/row.html' with article=article show_actions=True %}
3. Progressive Enhancement
<form action="{% url 'articles:create' %}" method="post"
hx-post="{% url 'articles:create' %}"
hx-target="#article-list">
<!-- Works with and without JavaScript -->
</form>
Final Thoughts
HTMX + Django 4.2 is a mature, productive stack. You get:
- Modern UX without a JavaScript framework
- Django’s batteries included
- Progressive enhancement
- Simpler architecture
For many applications, it’s all you need.
Full-stack Python, modern UX.