--- 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 ```bash pip install django-htmx ``` Add to `INSTALLED_APPS`: ```python INSTALLED_APPS = [ ... "django_htmx", ] ``` Add the middleware: ```python 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: ```python 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 htmx - `request.htmx.boosted` - True if request is from boosted element (hx-boost) - `request.htmx.current_url` - Current URL in browser from HX-Current-URL header - `request.htmx.current_url_abs_path` - Absolute path form of current_url - `request.htmx.history_restore_request` - True if request is for history restoration - `request.htmx.target` - Target element ID from HX-Target header - `request.htmx.trigger` - Trigger element ID from HX-Trigger header - `request.htmx.trigger_name` - Trigger element name from HX-Trigger-Name header - `request.htmx.prompt` - User response to hx-prompt attribute - `request.htmx.triggering_event` - Deserialized JSON from event-header extension ## Template Tags Load and use in templates: ```django {% load django_htmx %} {% htmx_script %} ... ``` ### Django Templates ```django {% load django_htmx %} {% htmx_script %} ... ``` Use `minified=False` for debugging: ```django {% htmx_script minified=False %} ``` ### Jinja2 ```python 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 ``` ```html {{ htmx_script() }} ``` ## HTTP Response Classes ### HttpResponseClientRedirect Triggers a client-side redirect (HX-Redirect header): ```python 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): ```python 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): ```python 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": ```python from django_htmx.http import HttpResponseStopPolling def my_pollable_view(request): if event_finished(): return HttpResponseStopPolling() ... ``` Or use the constant directly: ```python 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: ```python 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: ```python 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: ```python 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: ```python 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: ```python 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: ```python 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: ```bash pip install django-template-partials ``` ```django {% extends "_base.html" %} {% load partials %} {% block main %} {% partialdef country-table inline %} ...
{% endpartialdef %} {% endblock main %} ``` In views: ```python 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 ```python 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}) ``` ```django {% extends base_template %} {% block main %} ... {% endblock %} ``` ### CSRF Protection Always include CSRF token in htmx requests: ```html ``` ### Caching with HTMX Add appropriate Vary headers for cacheable responses: ```python 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): ```bash curl -L https://unpkg.com/htmx-ext-ws/dist/ws.min.js -o static/htmx-ext-ws.min.js ``` ```django {% load django_htmx static %} {% htmx_script %} ... ``` ## Type Checking For type-checking, extend HttpRequest: ```python 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/