Source code for htmx_nav.partials
"""
PartialSpec: what template or block to render for a given HTMX request.
"""
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from typing import Protocol, TypeAlias, runtime_checkable
from django.http import HttpRequest
from .targeting import Target, _eval_target
[docs]
@runtime_checkable
class PartialResolver(Protocol):
"""Derives a partial block or template path from the request and base template."""
[docs]
def resolve(self, request: HttpRequest, template_name: str) -> str | None:
"""Resolve a partial block or template path given the request and base template.
Args:
request: The incoming HTTP request.
template_name: The base template path specified by the view.
Returns:
A block name (e.g. ``"#content"``), a template path (e.g. ``"partials/_board.html"``),
or ``None`` to render the full template.
"""
...
[docs]
@dataclass(frozen=True)
class PathReplace:
"""Swap a path segment or directory, e.g. ``"pages/"`` -> ``"partials/_"``.
Particularly useful in pre-Django 6 codebases or multi-file template
layouts where full pages and partials live in separate directories.
Args:
old: The path segment to match (e.g. ``"pages/"``).
new: The replacement segment (e.g. ``"partials/_"``).
"""
old: str
new: str
[docs]
def resolve(self, request: HttpRequest, template_name: str) -> str | None:
"""Derive a partial template path by replacing ``old`` with ``new``.
If ``template_name`` does not contain ``old``, returns ``template_name``
unmodified so non-matching templates fall back gracefully.
"""
if template_name and self.old in template_name:
return template_name.replace(self.old, self.new, 1)
return template_name
PartialSpec: TypeAlias = (
str
| Callable[[HttpRequest], str | None]
| Mapping[str | PartialResolver, Target]
| PartialResolver
| None
)
"""Specifies what template or partial block to render for an HTMX request.
Values resolve to:
- Block name (``"#name"``): Appended to the base template, giving
``template.html#name`` (Django 6 native ``{% partialdef %}``).
- Standalone path (``"path/to/template.html"``): Rendered in place of the
base template.
- ``PartialResolver`` (e.g. ``PathReplace``): Derives a block name or path
from the request and the base template name. Returns ``template_name``
unmodified if it cannot resolve the given template.
- Callable ``(request) -> str | None``: Returns a block name, template path,
or ``None`` per request.
- Mapping: Keys are block names, paths, or resolvers; values are ``Target``
conditions. The first key whose condition matches wins, so end with
``True`` for a fallback.
- ``None``: Forces a full-page render.
Examples:
.. code-block:: python
# Block name (single-file, Django 6 inline partial)
"#content"
# Standalone path
"partials/_tab_content.html"
# Block inside another template
"partials/navigation_components.html#sidebar"
# Derived path: base template "pages/board.html"
# renders "partials/_board.html" on HTMX requests
PathReplace("pages/", "partials/_")
# Per-request callable
lambda request: "#tab_content" if htmx_target_is(request, "tabs") else "#content"
# Mapping, first match wins, mixing every key kind
{
"partials/_tab_content.html": targeting("tab-content"),
PathReplace("pages/", "partials/_"): targeting("main-content"),
"#content": True, # fallback
}
"""
def _resolve_partial_name(
partial: PartialSpec, request: HttpRequest, template_name: str = ""
) -> str | None:
"""Resolve the active partial or template name for a request."""
if partial is None:
return None
if isinstance(partial, str):
return partial
if isinstance(partial, PartialResolver):
return partial.resolve(request, template_name)
if isinstance(partial, Mapping):
for key, target in partial.items():
if _eval_target(target, request):
if isinstance(key, PartialResolver):
return key.resolve(request, template_name)
return key
return None
if callable(partial):
return partial(request)
raise TypeError(f"Invalid PartialSpec value: {partial!r}")
def _resolve_template_name(
template_name: str,
partial_name: str | None,
is_htmx: bool,
) -> str:
"""Resolve the final template path or block string to render."""
if not is_htmx or not partial_name:
return template_name
if partial_name.startswith("#"):
return f"{template_name}{partial_name}"
return partial_name