diff --git a/HISTORY.rst b/HISTORY.rst
index aaceaa99..e441a5da 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -14,6 +14,10 @@ History
* Add ``django_htmx.http.retarget()`` for setting the ``HX-Retarget`` header added in `htmx 1.6.1 `__.
+* Add ``HttpResponseLocation`` for sending a response with the ``HX-Location`` header.
+
+ Thanks to Ben Beecher in `PR #239 `__.
+
1.12.2 (2022-08-31)
-------------------
diff --git a/docs/http.rst b/docs/http.rst
index daa7069b..6d330861 100644
--- a/docs/http.rst
+++ b/docs/http.rst
@@ -3,6 +3,9 @@ HTTP
.. currentmodule:: django_htmx.http
+Response classes
+----------------
+
.. autoclass:: HttpResponseClientRedirect
htmx can trigger a client side redirect when it receives a response with the |HX-Redirect header|__.
@@ -44,6 +47,34 @@ HTTP
return HttpResponseClientRefresh()
...
+.. autoclass:: HttpResponseLocation
+
+ An HTTP response class for sending the |HX-Location header|__.
+ This header makes htmx make a client-side “boosted” request, acting like a client side redirect with a page reload.
+
+ .. |HX-Location header| replace:: ``HX-Location`` header
+ __ https://htmx.org/headers/hx-location/
+
+ ``redirect_to`` should be the URL to redirect to, as per Django’s |HttpResponseRedirect|__.
+
+ .. |HttpResponseRedirect| replace:: ``HttpResponseRedirect``
+ __ https://docs.djangoproject.com/en/stable/ref/request-response/#django.http.HttpResponseRedirect
+
+ ``source``, ``event``, ``target``, ``swap``, ``values``, and ``headers`` are all optional, with meaning as `documented by htmx `__.
+
+ For example:
+
+ .. code-block:: python
+
+ from django_htmx.http import HttpResponseLocation
+
+
+ def wait_for_completion(request, action_id):
+ ...
+ if action.completed:
+ return HttpResponseLocation(f"/action/{action.id}/completed/")
+ ...
+
.. autoclass:: HttpResponseStopPolling
When using a `polling trigger `__, htmx will stop polling when it encounters a response with the special HTTP status code 286.
@@ -80,6 +111,9 @@ HTTP
return render("event-finished.html", status=HTMX_STOP_POLLING)
...
+Response modifying functions
+----------------------------
+
.. autofunction:: push_url
Set the |HX-Push-Url header|__ of ``response`` and return it.
diff --git a/src/django_htmx/http.py b/src/django_htmx/http.py
index 7ddf1088..a71cc372 100644
--- a/src/django_htmx/http.py
+++ b/src/django_htmx/http.py
@@ -48,6 +48,51 @@ def __init__(self) -> None:
self["HX-Refresh"] = "true"
+class HttpResponseLocation(HttpResponseRedirectBase):
+ status_code = 200
+
+ def __init__(
+ self,
+ redirect_to: str,
+ *args: Any,
+ source: str | None = None,
+ event: str | None = None,
+ target: str | None = None,
+ swap: Literal[
+ "innerHTML",
+ "outerHTML",
+ "beforebegin",
+ "afterbegin",
+ "beforeend",
+ "afterend",
+ "delete",
+ "none",
+ None,
+ ] = None,
+ values: dict[str, str] | None = None,
+ headers: dict[str, str] | None = None,
+ **kwargs: Any,
+ ) -> None:
+ super().__init__(redirect_to, *args, **kwargs)
+ spec: dict[str, str | dict[str, str]] = {
+ "path": self["Location"],
+ }
+ del self["Location"]
+ if source is not None:
+ spec["source"] = source
+ if event is not None:
+ spec["event"] = event
+ if target is not None:
+ spec["target"] = target
+ if swap is not None:
+ spec["swap"] = swap
+ if headers is not None:
+ spec["headers"] = headers
+ if values is not None:
+ spec["values"] = values
+ self["HX-Location"] = json.dumps(spec)
+
+
_HttpResponse = TypeVar("_HttpResponse", bound=HttpResponseBase)
diff --git a/tests/test_http.py b/tests/test_http.py
index 22eadda7..7304596e 100644
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import json
from uuid import UUID
import pytest
@@ -9,6 +10,7 @@
from django_htmx.http import HttpResponseClientRedirect
from django_htmx.http import HttpResponseClientRefresh
+from django_htmx.http import HttpResponseLocation
from django_htmx.http import HttpResponseStopPolling
from django_htmx.http import push_url
from django_htmx.http import reswap
@@ -41,6 +43,40 @@ def test_repr(self):
)
+class HttpResponseLocationTests(SimpleTestCase):
+ def test_success(self):
+ response = HttpResponseLocation("/home/")
+
+ assert response.status_code == 200
+ assert "Location" not in response
+ spec = json.loads(response["HX-Location"])
+ assert spec == {"path": "/home/"}
+
+ def test_success_complete(self):
+ response = HttpResponseLocation(
+ "/home/",
+ source="#button",
+ event="doubleclick",
+ target="#main",
+ swap="innerHTML",
+ headers={"year": "2022"},
+ values={"banner": "true"},
+ )
+
+ assert response.status_code == 200
+ assert "Location" not in response
+ spec = json.loads(response["HX-Location"])
+ assert spec == {
+ "path": "/home/",
+ "source": "#button",
+ "event": "doubleclick",
+ "target": "#main",
+ "swap": "innerHTML",
+ "headers": {"year": "2022"},
+ "values": {"banner": "true"},
+ }
+
+
class HttpResponseClientRefreshTests(SimpleTestCase):
def test_success(self):
response = HttpResponseClientRefresh()