Skip to main content

django-htmx plus some extras

Project description

django-htmx-plus

A Django utility library that works with django-cotton to provide ready-made views, mixins, middleware, response helpers, and components for building HTMX-powered list views with filtering, sorting, and pagination.

This package was created for my own projects but it was handy enough to share, I learned how to use HTMX with Django and Boostrap Modals from the following two posts from Josh Karamuth:


Features

  • HtmxResponse – a 204 No Content response that fires HX-Trigger events on the client.
  • HtmxRedirectResponse – a response that sends an HX-Redirect header to navigate the browser.
  • HtmxMessagesMiddleware – automatically forwards Django messages to the client via the HX-Trigger header on every HTMX request.
  • HtmxFormResponseMixin – a FormView/CreateView mixin that replaces the success redirect with an HTMX trigger response.
  • HtmxListView – a ListView subclass with built-in URL-based filtering, column sorting, and elided pagination.
  • Filter helpers – parse field.filter_type=value query parameters safely into Django ORM filter dicts.
  • Cotton components – drop-in table, header cell, and pager components styled for Bootstrap 5.
  • Template filtersget_attr and get_key_value for accessing object attributes and dict values in templates.
  • Built-in JavaScript helper – a django-htmx-plus.js ES module that wires Bootstrap 5 Modals and Offcanvases to HTMX swap events, with a {% htmx_plus_script %} template tag to include it.

Requirements

  • Python 3.11+
  • Django (any version compatible with the above)
  • django-cotton ≥ 2.6

Installation

pip install django-htmx-plus

Add to INSTALLED_APPS and configure middleware in settings.py:

INSTALLED_APPS = [
    # ...
    "django_cotton",
    "django_htmx_plus",
]

MIDDLEWARE = [
    # ...
    "django_htmx_plus.middleware.HtmxMessagesMiddleware",
]

Usage

HtmxResponse

Return a 204 No Content response that triggers one or more HTMX events on the client:

from django_htmx_plus.http import HtmxResponse

def my_view(request):
    # ... do some work ...
    return HtmxResponse(triggers=["itemUpdated", "refreshStats"])

The response sets HX-Trigger: itemUpdated,refreshStats, which HTMX picks up to re-fetch any elements listening for those events.


HtmxRedirectResponse

Instruct HTMX to navigate the browser to a new URL without a traditional HTTP redirect:

from django_htmx_plus.http import HtmxRedirectResponse

def my_view(request):
    return HtmxRedirectResponse(destination="/dashboard/")

HtmxMessagesMiddleware

Once the middleware is installed, any Django message added during an HTMX request is automatically serialised into the HX-Trigger header as a messages key:

{
  "messages": [
    {"message": "Record saved.", "tags": "success"}
  ]
}

If the view already sets its own HX-Trigger header (either string or JSON object syntax), the middleware merges the messages in without overwriting existing triggers.

Listen for the messages event in your HTMX setup to display the messages, for example:

document.body.addEventListener("messages", (event) => {
    event.detail.value.forEach(({message, tags}) => {
        showToast(message, tags); // your toast implementation
    });
});

HtmxFormResponseMixin

Mix into any FormView or CreateView to replace the default success redirect with an HTMX trigger response:

from django.views.generic.edit import CreateView
from django_htmx_plus.mixins import HtmxFormResponseMixin
from myapp.models import Article
from myapp.forms import ArticleForm

class ArticleCreateView(HtmxFormResponseMixin, CreateView):
    model = Article
    form_class = ArticleForm
    template_name = "articles/form.html"

    valid_triggers = ["articleCreated"]
    success_message = "Article created successfully."

After a valid submission, form.save() is called, an optional Django success message is queued, and an HtmxResponse carrying articleCreated in HX-Trigger is returned.

Attribute Type Description
valid_triggers List[str] Events to include in HX-Trigger on success.
success_message str Optional Django success message to queue.

HtmxListView

A drop-in replacement for Django's ListView that adds URL-driven filtering, sorting, and elided pagination:

from django.views.generic import ListView
from django_htmx_plus.views import HtmxListView
from myapp.models import Article

class ArticleListView(HtmxListView):
    model = Article
    template_name = "articles/list.html"
    paginate_by = 20
    target_id = "#article-table"

    # Restrict filtering and sorting to these fields only
    fields = ("id", "title", "status", "created_at")

    # Optional custom column labels
    labels = {
        "created_at": "Date Created",
    }

URL-based filtering

Filters are expressed as field_name.filter_type=value query parameters:

Filter key Django lookup Example
eq exact status.eq=published
ieq iexact title.ieq=hello
gt / gte / lt / lte gt / gte / lt / lte views.gte=100
like / ilike like / ilike title.ilike=django
sw / isw startswith / istartswith title.sw=Django
ew / iew endswith / iendswith title.ew=plus
in in status.in=['draft','published']
nl isnull deleted_at.nl=True
rng range created_at.rng=['2024-01-01','2024-12-31']
sch search body.sch=htmx

Only fields listed in fields are accepted. Setting fields = ("__all__",) lifts the restriction.

Sorting

Add order_by=field_name (or order_by=-field_name for descending) to the query string. Only fields in fields are permitted.

Context variables

Variable Description
query_params URL-encoded query string (without page).
page_range Elided page range for pagination controls.
order_by The currently active ordering field.
path The current request path.
target_id The HTMX target element ID.
query Full query string including order_by.
filter_query Query string containing only filter parameters.
filters Template-ready dict mapping plain field names to their filter values.
fields Dict with keys (field names) and labels (display names).

Cotton Table Components

django-htmx-plus ships a set of django-cotton components for rendering sortable, paginated HTMX tables with Bootstrap 5.

<c-tables.htmx_table />

Renders a full table with auto-generated sortable headers, rows, and an optional pager:

{% load cotton_extras %}

<c-tables.htmx_table class="table table-striped" />

The component uses the fields, objects, order_by, path, filter_query, target_id, page_obj, and paginator context variables provided automatically by HtmxListView.

<c-tables.header_cell name="field_name" />

Renders a single <th> with an hx-get attribute that toggles ascending/descending order and a chevron icon indicating the current sort direction:

<c-tables.head>
    <c-tables.header_cell name="title">Title</c-tables.header_cell>
    <c-tables.header_cell name="created_at">Date</c-tables.header_cell>
</c-tables.head>

<c-tables.pager />

Renders a Bootstrap 5 pagination control with previous/next buttons and elided page numbers, all wired to hx-get.

Icon components

Component Description
<c-icons.chevron_up /> Up chevron (ascending sort indicator).
<c-icons.chevron_down /> Down chevron (descending sort indicator).
<c-icons.chevron_left /> Left chevron (previous page).
<c-icons.chevron_right /> Right chevron (next page).

All icon components accept an optional add_class attribute to append extra CSS classes.


Template filters

Load the cotton_extras tag library in any template:

{% load cotton_extras %}
Filter Description Example
get_attr Get an attribute from an object by name. {{ item|get_attr:"title" }}
get_key_value Get a value from a dict by key. {{ my_dict|get_key_value:"name" }}

Built-in JavaScript Helper

django-htmx-plus ships a small ES module (django-htmx-plus.js) that integrates Bootstrap 5 Modals and Offcanvases with HTMX swap events, so they open and close automatically based on HTMX responses.

How it works

Behaviour Trigger
Show a Modal or Offcanvas HTMX swaps content into the element → htmx:afterSwap fires and calls .show().
Hide a Modal or Offcanvas HTMX receives an empty response targeting the element → htmx:beforeSwap fires, calls .hide(), and cancels the swap.
Reset Modal body Bootstrap's hidden.bs.modal event fires → the modal body is cleared to "".

Setup

Mark your Bootstrap Modal root elements with data-htmx-plus-modal="<id>" and your Offcanvas root elements with data-htmx-plus-offcanvas="<id>", where id is the id of the element that will be swapped,:

<!-- Bootstrap Modal managed by django-htmx-plus -->
<div class="modal fade" data-htmx-plus-modal="dialog">
    <div id="dialog" class="modal-dialog">
        <!-- Modal content will be swapped here by HTMX and shown/hidden by django-htmx-plus.js -->
    </div>
</div>

<!-- Bootstrap Offcanvas managed by django-htmx-plus -->
<div id="flyout" class="offcanvas offcanvas-end" data-htmx-plus-offcanvas="flyout">
    <!-- Offcanvas content will be swapped here by HTMX and shown/hidden by django-htmx-plus.js -->
</div>

Then point an HTMX element at the matching target ID:

<button hx-get="/person/add/" hx-target="#dialog">
    Add Person
</button>
  • When the response has content the modal/offcanvas is shown automatically.
  • When the server returns an empty 200 (or you use HtmxResponse) the modal/offcanvas is hidden automatically.

Including the script

Use the {% htmx_plus_script %} template tag to render the <script> tag. The script is loaded as an ES module and optionally forwards a CSP nonce if one is present in the template context:

{% load django_htmx_plus %}

<!-- Place near the bottom of your base template, after Bootstrap JS -->
{% htmx_plus_script %}

This renders:

<script src="/static/django_htmx_plus/django-htmx-plus.js" type="module"></script>

If a nonce variable is present in the template context it is automatically added as a nonce="..." attribute.

Note: The script imports Modal and Offcanvas from bootstrap, so Bootstrap 5 must be available as an ES module (e.g. via an import map or a bundler). If you load Bootstrap as a plain global script instead, adjust your bundler or import map accordingly.

For example

<script type="importmap">
{
  "imports": {
	"@popperjs/core": "{% static '@popperjs/core/dist/esm/index.js' %}",
	"bootstrap": "{% static 'bootstrap/js/index.esm.js' %}",
  }
}
</script>

License

MIT — see LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_htmx_plus-0.2.0.tar.gz (13.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_htmx_plus-0.2.0-py3-none-any.whl (19.8 kB view details)

Uploaded Python 3

File details

Details for the file django_htmx_plus-0.2.0.tar.gz.

File metadata

  • Download URL: django_htmx_plus-0.2.0.tar.gz
  • Upload date:
  • Size: 13.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.1.3 CPython/3.11.9 Windows/10

File hashes

Hashes for django_htmx_plus-0.2.0.tar.gz
Algorithm Hash digest
SHA256 83c93057c43355911022b160a83f5c8da06ab0f0a068159c541da0315522e51c
MD5 d12eed705094ccf196bf2392e33e1f0e
BLAKE2b-256 93a252e5dd06f1f06ac818e79a4bd53866201be54e1c6b1c08b50cc95ad40380

See more details on using hashes here.

File details

Details for the file django_htmx_plus-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: django_htmx_plus-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 19.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.1.3 CPython/3.11.9 Windows/10

File hashes

Hashes for django_htmx_plus-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 327994336d2ed91cf0584543477566b832b410cb2bbe08648f085f908967f833
MD5 026c7bf38158ac79531347790a0e4e2a
BLAKE2b-256 215d7d6030e742c53b11487a2cdb71792f02f8d2c872c116bf52ab568613119c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page