npx claudepluginhub preludetech/django-craftThis skill uses the workspace's default tool permissions.
HTMX is loaded globally in `_base.html`. CSRF is handled globally via `hx-headers` on the `<body>` tag:
Implements HTMX in Drupal 11.3+ for dynamic interactions like dependent dropdowns, infinite scroll, real-time validation, multi-step wizards. Guides AJAX migration.
Share bugs, ideas, or general feedback.
HTMX is loaded globally in _base.html. CSRF is handled globally via hx-headers on the <body> tag:
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
Never add CSRF tokens to individual HTMX requests.
is_htmx = request.headers.get("HX-Request") == "true"
Use standard render() by default:
def partial_course_toc(request, slug):
# ...
return render(request, "app/partials/course_toc.html", context)
Use render_to_string() only when composing HTML strings into a larger response (e.g., panel systems that assemble multiple partials):
content = render_to_string("app/partials/panel.html", context, request=request)
When a view serves both HTMX and full-page requests, return partial content for HTMX and the full wrapped response for normal requests:
def render(self, request, ...) -> str:
is_htmx = request.headers.get("HX-Request") == "true"
if is_htmx:
return self.get_content(request, ...)
return self.get_full_response(request, ...)
Prefix view functions/methods returning HTMX partials with partial_ (e.g., partial_course_toc, partial_list_courses).
hx-target and hx-swap Together<div hx-get="{% url 'app:endpoint' %}"
hx-target="#content"
hx-swap="outerHTML">
Prefer outerHTML as the swap strategy.
<div hx-get="{% url 'app:partial_endpoint' %}"
hx-trigger="load"
hx-target="#content"
hx-swap="outerHTML"
hx-indicator="#loader">
<c-loading-indicator id="loader" message="Loading..." />
</div>
<form hx-post="{% url 'app:submit' %}"
hx-target="#result"
hx-swap="outerHTML">
<c-button type="submit">Submit</c-button>
</form>
<form hx-get="{{ base_url }}"
hx-target="#table-container"
hx-trigger="submit, input delay:300ms from:#search-input"
hx-include="#search-input"
hx-swap="outerHTML">
<input type="search" id="search-input" name="search" />
</form>
Preserve sort, search, and pagination state in query parameters, not request bodies:
<a hx-get="{{ base_url }}?sort=name&order=asc&page=2"
hx-target="#table-container"
hx-swap="outerHTML">
Sort by Name
</a>
Use hx-boost="false" on links that should do full-page navigation (e.g., admin, external links):
<c-button href="/admin/" hx-boost="false">Admin Panel</c-button>
Use hx-indicator with the <c-loading-indicator> component:
<div hx-get="{% url 'app:data' %}"
hx-trigger="load"
hx-indicator="#loader">
<c-loading-indicator id="loader" message="Loading data..." />
</div>
Use the button component's loading prop with CSS utility classes:
<c-button type="submit" loading loading_text="Saving...">Save</c-button>
The CSS classes .htmx-hide-on-request and .htmx-show-on-request toggle visibility during HTMX requests (defined in tailwind.components.css).
Do not use Alpine.js for things that should be server round-trips, and do not use HTMX for purely client-side UI state.
Simulate HTMX requests in tests by setting the HX-Request header:
response = client.get("/endpoint/", HTTP_HX_REQUEST="true")
Assert that HTMX responses return partial content without full-page wrappers:
assert "<section" not in response.content.decode()