| id | 23 |name | 24 |
|---|---|
| {{ person.id }} | 30 |{{ person.name }} | 31 |
| 35 | No people on this page. 36 | | 37 ||
6 | This form shows you how to implement CSRF with htmx, using the hx-headers attribute.
7 |
11 | View the source to see how it works! 12 |
13 |Awaiting interaction...
41 |6 | This page shows you the django-htmx extension script error handler in action. 7 |
8 |11 | See more in the docs. 12 |
13 |
16 | {% if DEBUG %}
17 | The error handler will work, since DEBUG = True.
18 | {% else %}
19 | The error handler will not work, since DEBUG = False.
20 | {% endif %}
21 |
Awaiting interaction...
46 || Attribute | 7 |Value | 8 |
|---|---|
| Timestamp | 13 |{{ timestamp }} | 14 |
request.method |
17 | {{ request.method|stringformat:'r' }} |
18 |
bool(request.htmx) |
21 |
22 | {% if request.htmx %}
23 | True
24 | {% else %}
25 | For
26 | {% endif %}
27 | |
28 |
request.htmx.boosted |
31 | {{ request.htmx.boosted|stringformat:'r' }} |
32 |
request.htmx.current_url |
35 | {{ request.htmx.current_url|stringformat:'r' }} |
36 |
request.htmx.current_url_abs_path |
39 | {{ request.htmx.current_url_abs_path|stringformat:'r' }} |
40 |
request.htmx.prompt |
43 | {{ request.htmx.prompt|stringformat:'r' }} |
44 |
request.htmx.target |
47 | {{ request.htmx.target|stringformat:'r' }} |
48 |
request.htmx.trigger |
51 | {{ request.htmx.trigger|stringformat:'r' }} |
52 |
request.htmx.trigger_name |
55 | {{ request.htmx.trigger_name|stringformat:'r' }} |
56 |
59 | request.htmx.triggering_event 60 | (via event-header extension) 61 | |
62 |
63 | {% if request.htmx.triggering_event %}
64 |
65 |
68 | {% else %}
69 | JSON66 |
67 | {{ request.htmx.triggering_event|stringformat:'r' }}
70 | {% endif %}
71 | |
72 |
request.POST.get('keyup_input') |
75 | {{ request.POST.keyup_input|stringformat:'r' }} |
76 |
7 | This example shows you how you can do partial rendering for htmx requests using django-template-partials. 8 | The view renders only the content of the table section partial for requests made with htmx, saving time and bandwidth. 9 | Paginate through the below list of randomly generated people to see this in action, and study the view and template. 10 |
11 |12 | See more in the docs. 13 |
14 || id | 23 |name | 24 |
|---|---|
| {{ person.id }} | 30 |{{ person.name }} | 31 |
| 35 | No people on this page. 36 | | 37 ||
This is our fancy custom 500 page.
" 91 | ) 92 | 93 | 94 | # Middleware tester 95 | 96 | # This uses two views - one to render the form, and the second to render the 97 | # table of attributes. 98 | 99 | 100 | @require_GET 101 | def middleware_tester(request: HtmxHttpRequest) -> HttpResponse: 102 | return render(request, "middleware-tester.html") 103 | 104 | 105 | @require_http_methods(["DELETE", "POST", "PUT"]) 106 | def middleware_tester_table(request: HtmxHttpRequest) -> HttpResponse: 107 | return render( 108 | request, 109 | "middleware-tester-table.html", 110 | {"timestamp": time.time()}, 111 | ) 112 | 113 | 114 | # Partial rendering example 115 | 116 | 117 | # This dataclass acts as a stand-in for a database model - the example app 118 | # avoids having a database for simplicity. 119 | 120 | 121 | @dataclass 122 | class Person: 123 | id: int 124 | name: str 125 | 126 | 127 | faker = Faker() 128 | people = [Person(id=i, name=faker.name()) for i in range(1, 235)] 129 | 130 | 131 | @require_GET 132 | def partial_rendering(request: HtmxHttpRequest) -> HttpResponse: 133 | # Standard Django pagination 134 | page_num = request.GET.get("page", "1") 135 | page = Paginator(object_list=people, per_page=10).get_page(page_num) 136 | 137 | # The htmx magic - render just the `#table-section` partial for htmx 138 | # requests, allowing us to skip rendering the unchanging parts of the 139 | # template. 140 | template_name = "partial-rendering.html" 141 | if request.htmx: 142 | template_name += "#table-section" 143 | 144 | return render( 145 | request, 146 | template_name, 147 | { 148 | "page": page, 149 | }, 150 | ) 151 | -------------------------------------------------------------------------------- /src/django_htmx/http.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | from typing import Any, Literal, TypeVar 5 | 6 | from django.core.serializers.json import DjangoJSONEncoder 7 | from django.http import HttpResponse 8 | from django.http.response import HttpResponseBase, HttpResponseRedirectBase 9 | 10 | HTMX_STOP_POLLING = 286 11 | 12 | SwapMethod = Literal[ 13 | "innerHTML", 14 | "outerHTML", 15 | "beforebegin", 16 | "afterbegin", 17 | "beforeend", 18 | "afterend", 19 | "delete", 20 | "none", 21 | ] 22 | 23 | 24 | class HttpResponseStopPolling(HttpResponse): 25 | status_code = HTMX_STOP_POLLING 26 | 27 | def __init__(self, *args: Any, **kwargs: Any) -> None: 28 | super().__init__(*args, **kwargs) 29 | self._reason_phrase = "Stop Polling" 30 | 31 | 32 | class HttpResponseClientRedirect(HttpResponseRedirectBase): 33 | status_code = 200 34 | 35 | def __init__(self, redirect_to: str, *args: Any, **kwargs: Any) -> None: 36 | if kwargs.get("preserve_request"): 37 | raise ValueError( 38 | "The 'preserve_request' argument is not supported for " 39 | "HttpResponseClientRedirect.", 40 | ) 41 | super().__init__(redirect_to, *args, **kwargs) 42 | self["HX-Redirect"] = self["Location"] 43 | del self["Location"] 44 | 45 | @property 46 | def url(self) -> str: 47 | return self["HX-Redirect"] 48 | 49 | 50 | class HttpResponseClientRefresh(HttpResponse): 51 | def __init__(self) -> None: 52 | super().__init__() 53 | self["HX-Refresh"] = "true" 54 | 55 | 56 | class HttpResponseLocation(HttpResponseRedirectBase): 57 | status_code = 200 58 | 59 | def __init__( 60 | self, 61 | redirect_to: str, 62 | *args: Any, 63 | source: str | None = None, 64 | event: str | None = None, 65 | target: str | None = None, 66 | swap: SwapMethod | None = None, 67 | select: str | None = None, 68 | values: dict[str, str] | None = None, 69 | headers: dict[str, str] | None = None, 70 | **kwargs: Any, 71 | ) -> None: 72 | super().__init__(redirect_to, *args, **kwargs) 73 | spec: dict[str, str | dict[str, str]] = { 74 | "path": self["Location"], 75 | } 76 | del self["Location"] 77 | if source is not None: 78 | spec["source"] = source 79 | if event is not None: 80 | spec["event"] = event 81 | if target is not None: 82 | spec["target"] = target 83 | if swap is not None: 84 | spec["swap"] = swap 85 | if select is not None: 86 | spec["select"] = select 87 | if headers is not None: 88 | spec["headers"] = headers 89 | if values is not None: 90 | spec["values"] = values 91 | self["HX-Location"] = json.dumps(spec) 92 | 93 | 94 | _HttpResponse = TypeVar("_HttpResponse", bound=HttpResponseBase) 95 | 96 | 97 | def push_url(response: _HttpResponse, url: str | Literal[False]) -> _HttpResponse: 98 | response["HX-Push-Url"] = "false" if url is False else url 99 | return response 100 | 101 | 102 | def replace_url(response: _HttpResponse, url: str | Literal[False]) -> _HttpResponse: 103 | response["HX-Replace-Url"] = "false" if url is False else url 104 | return response 105 | 106 | 107 | def reswap(response: _HttpResponse, method: SwapMethod) -> _HttpResponse: 108 | response["HX-Reswap"] = method 109 | return response 110 | 111 | 112 | def retarget(response: _HttpResponse, target: str) -> _HttpResponse: 113 | response["HX-Retarget"] = target 114 | return response 115 | 116 | 117 | def reselect(response: _HttpResponse, selector: str) -> _HttpResponse: 118 | response["HX-Reselect"] = selector 119 | return response 120 | 121 | 122 | def trigger_client_event( 123 | response: _HttpResponse, 124 | name: str, 125 | params: dict[str, Any] | None = None, 126 | *, 127 | after: Literal["receive", "settle", "swap"] = "receive", 128 | encoder: type[json.JSONEncoder] = DjangoJSONEncoder, 129 | ) -> _HttpResponse: 130 | params = params or {} 131 | 132 | if after == "receive": 133 | header = "HX-Trigger" 134 | elif after == "settle": 135 | header = "HX-Trigger-After-Settle" 136 | elif after == "swap": 137 | header = "HX-Trigger-After-Swap" 138 | else: 139 | raise ValueError( 140 | "Value for 'after' must be one of: 'receive', 'settle', or 'swap'." 141 | ) 142 | 143 | if header in response: 144 | value = response[header] 145 | try: 146 | data = json.loads(value) 147 | except json.JSONDecodeError as exc: 148 | raise ValueError(f"{header!r} value should be valid JSON.") from exc 149 | data[name] = params 150 | else: 151 | data = {name: params} 152 | 153 | response[header] = json.dumps(data, cls=encoder) 154 | 155 | return response 156 | -------------------------------------------------------------------------------- /docs/middleware.rst: -------------------------------------------------------------------------------- 1 | Middleware 2 | ========== 3 | 4 | .. currentmodule:: django_htmx.middleware 5 | 6 | .. class:: HtmxMiddleware 7 | 8 | This middleware attaches ``request.htmx``, an instance of :obj:`HtmxDetails` (below). 9 | Your views, and any following middleware, can use ``request.htmx`` to switch behaviour for requests from htmx. 10 | The middleware supports both sync and async modes. 11 | 12 | See it action in the “Middleware Tester” section of the :doc:`example project