├── LICENSE ├── README.md ├── pyproject.toml └── src └── starlette_htmx ├── __init__.py └── middleware.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Adam Johnson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Starlette_htmx 2 | 3 | A set of extensions for using [HTMX](https://htmx.org) with [Starlette](http://starlette.io). 4 | 5 | Based on [django-htmx](https://github.com/adamchainz/django-htmx) by [Adam Johnson](https://github.com/adamchainz) 6 | 7 | ## Middleware 8 | 9 | Usage: 10 | 11 | ```python 12 | from starlette.middleware import Middleware 13 | from starlette_htmx.middleware import HtmxMiddleware 14 | 15 | app = Starlette(debug=True, ..., middleware=[Middleware(HtmxMiddleware)]) 16 | ``` 17 | 18 | The request objects will then have a `request.state.htmx` object which you can 19 | use to test whether the request was made by htmx. 20 | 21 | 22 | This class provides shortcuts for reading the htmx-specific `request headers `__. 23 | 24 | ### `__bool__(): bool` 25 | 26 | `True` if the request was made with htmx, otherwise `False`. 27 | This is based on the presence of the `HX-Request` header. 28 | 29 | This allows you to switch behaviour for requests made with htmx like so: 30 | 31 | ```python 32 | 33 | def my_view(request): 34 | if request.htmx: 35 | template_name = "partial.html" 36 | else: 37 | template_name = "complete.html" 38 | return render(template_name, ...) 39 | ``` 40 | 41 | ### `boosted: bool` 42 | 43 | `True` if the request came from an element with the `hx-boost` attribute. 44 | Based on the `HX-Boosted` header. 45 | 46 | ### `current_url: str | None` 47 | 48 | The current URL of the browser, or `None` for non-htmx requests. 49 | Based on the `HX-Current-URL` header. 50 | 51 | ### `history_restore_request: bool` 52 | 53 | `True` if the request is for history restoration after a miss in the local history cache. 54 | Based on the `HX-History-Restore-Request` header. 55 | 56 | ### `prompt: str | None` 57 | 58 | The user response to `hx-prompt `__ if it was used, or `None`. 59 | 60 | ### `target: str | None` 61 | 62 | The `id` of the target element if it exists, or `None`. 63 | Based on the `HX-Target` header. 64 | 65 | ### `trigger: str | None` 66 | 67 | The `id` of the triggered element if it exists, or `None`. 68 | Based on the `HX-Trigger` header. 69 | 70 | ### `trigger_name: str | None` 71 | 72 | The `name` of the triggered element if it exists, or `None`. 73 | Based on the `HX-Trigger-Name` header. 74 | 75 | ### `triggering_event: Any | None` 76 | 77 | The deserialized JSON representtation of the event that triggered the request if it exists, or `None`. 78 | This header is set by the `event-header htmx extension 79 | `__, and contains details of the DOM 80 | event that triggered the request. 81 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "starlette_htmx" 7 | authors = [{name = "Felix Ingram", email = "f.ingram@gmail.com"}] 8 | license = {file = "LICENSE"} 9 | classifiers = ["License :: OSI Approved :: MIT License"] 10 | dynamic = ["version", "description"] 11 | 12 | [project.urls] 13 | Home = "https://github.com/lllama/starlette-htmx" 14 | -------------------------------------------------------------------------------- /src/starlette_htmx/__init__.py: -------------------------------------------------------------------------------- 1 | """Extensions for using HTMX with Starlette""" 2 | 3 | __version__ = "0.1" 4 | -------------------------------------------------------------------------------- /src/starlette_htmx/middleware.py: -------------------------------------------------------------------------------- 1 | import json 2 | from functools import cached_property 3 | from typing import Any, Optional 4 | from urllib.parse import unquote 5 | 6 | from starlette.middleware.base import BaseHTTPMiddleware 7 | 8 | 9 | class HtmxDetails: 10 | def __init__(self, request) -> None: 11 | self.request = request 12 | 13 | def _get_header_value(self, name: str) -> Optional[str]: 14 | value = self.request.headers.get(name) or None 15 | if value: 16 | if self.request.headers.get(f"{name}-URI-AutoEncoded") == "true": 17 | value = unquote(value) 18 | return value 19 | 20 | def __bool__(self) -> bool: 21 | return self._get_header_value("HX-Request") == "true" 22 | 23 | @cached_property 24 | def boosted(self) -> bool: 25 | return self._get_header_value("HX-Boosted") == "true" 26 | 27 | @cached_property 28 | def current_url(self) -> Optional[str]: 29 | return self._get_header_value("HX-Current-URL") 30 | 31 | @cached_property 32 | def history_restore_request(self) -> bool: 33 | return self._get_header_value("HX-History-Restore-Request") == "true" 34 | 35 | @cached_property 36 | def prompt(self) -> Optional[str]: 37 | return self._get_header_value("HX-Prompt") 38 | 39 | @cached_property 40 | def target(self) -> Optional[str]: 41 | return self._get_header_value("HX-Target") 42 | 43 | @cached_property 44 | def trigger(self) -> Optional[str]: 45 | return self._get_header_value("HX-Trigger") 46 | 47 | @cached_property 48 | def trigger_name(self) -> Optional[str]: 49 | return self._get_header_value("HX-Trigger-Name") 50 | 51 | @cached_property 52 | def triggering_event(self) -> Any: 53 | value = self._get_header_value("Triggering-Event") 54 | if value is not None: 55 | try: 56 | value = json.loads(value) 57 | except json.JSONDecodeError: 58 | value = None 59 | return value 60 | 61 | 62 | class HtmxMiddleware(BaseHTTPMiddleware): 63 | async def dispatch(self, request, call_next): 64 | details = HtmxDetails(request) 65 | request.state.htmx = details 66 | return await call_next(request) 67 | --------------------------------------------------------------------------------