Django forms mixin for managing form updates using htmx triggers
Project description
django-hx-forms
Django forms mixin for managing form updates using htmx triggers
django-hx-forms provides a mixin for Django forms to allow related form fields to be updated via HTMX events. No javascript is required (aside from HTMX itself). All the logic surrounding related fields is handled with the Django form. Examples:
- A choice field needs updating based on another choice field being selected
- A field needs hiding based on another field's state
- A field needs resetting based on another field's state
- A field needs disabling based on another field's state
The form allows rules to be defined to identify which fields should trigger an update, and which fields are affected by this trigger.
Installation
With uv:
uv add django-hx-forms
With pip:
pip install django-hx-forms
Or add it to your pyproject.toml dependencies directly:
dependencies = [
"django-hx-forms",
]
You also need HTMX loaded in your templates — see the HTMX installation instructions for the current options.
Usage
A reactive form is made of three parts: a form that declares its trigger fields and how it reacts, a view that re-renders the form when a trigger fires, and a template partial shared between the initial page and the HTMX response.
The examples below use a country/region form, where choosing a country narrows the available regions.
Form mixin
Subclass HtmxModelForm (a ModelForm with the mixin applied) or add HtmxFormMixin to a plain Form.
Point the hx_* attributes at your update endpoint, declare the reactive rules in Meta, and override
check_form_state() to adjust the form for the current values.
from django.urls import reverse_lazy
from django_hx_forms.forms import HtmxModelForm
from .models import Address
class AddressForm(HtmxModelForm):
hx_post = reverse_lazy("addresses:form-update") # endpoint HTMX posts to
hx_target = "#address-fields" # element swapped with the response
hx_indicator = "#form-loading" # optional loading indicator
default_focus_field = "country" # field autofocused on first render
def check_form_state(self):
"""Adjust field state for the current values. Runs on every render."""
country = self.get_field_value("country")
if country:
self.enable_field("region")
self.set_field_required("region", True)
self.set_field_queryset("region", Region.objects.filter(country=country))
else:
self.disable_field("region")
self.set_field_required("region", False)
class Meta:
model = Address
fields = ["country", "region"]
# Changing `country` fires an HTMX request to re-render the form…
htmx_trigger_fields = ["country"]
# …and clears `region` so a stale value can't survive the change
htmx_field_resets = {"country": ["region"]}
hx_post/hx_target/hx_indicator— the mixin attaches the matchinghx-post,hx-targetandhx-indicatorattributes to each field inMeta.htmx_trigger_fields; you don't write them in the template.htmx_trigger_fields— fields whose change re-renders the form.htmx_field_resets— a{trigger_field: [fields_to_clear]}map applied before re-rendering.check_form_state()— enable/disable fields, swap choices or querysets, and toggle required state here. The view calls it on each update; call it from__init__too if you want it applied on the first render.
View mixin
HtmxFormUpdateViewMixin serves the re-rendered partial. It reads the trigger from the HX-Trigger-Name
header, applies the resets, calls check_form_state(), and rejects non-HTMX requests with a 403.
from django.shortcuts import get_object_or_404
from django_hx_forms.views import HtmxFormUpdateViewMixin
from .forms import AddressForm
from .models import Address
class AddressFormUpdateView(HtmxFormUpdateViewMixin):
form_class = AddressForm
template_name = "addresses/_address_fields.html"
def get_form_instance(self):
"""Return the instance being edited (ModelForms only), or None when adding."""
address_id = self.request.POST.get("address_id")
return get_object_or_404(Address, pk=address_id) if address_id else None
Route it at the URL named in hx_post:
path("form-update/", AddressFormUpdateView.as_view(), name="form-update"),
Your ordinary CreateView / UpdateView reuse the same form and don't need this mixin — they render the
full page, and the update view only handles the reactive refresh.
Templates
Keep the reactive fields in a partial so the initial page and the HTMX response render identically:
{# addresses/_address_fields.html #}
{{ form.country.label_tag }} {{ form.country }}
{{ form.region.label_tag }} {{ form.region }}
The full page wraps that partial in the hx_target element (plus the optional hx_indicator):
<form method="post">
{% csrf_token %}
{% if object %}<input type="hidden" name="address_id" value="{{ object.pk }}">{% endif %}
<div id="form-loading" class="htmx-indicator">Updating…</div>
<div id="address-fields">
{% include "addresses/_address_fields.html" %}
</div>
<button type="submit">Save</button>
</form>
Changing the country field posts to the update view and swaps in a refreshed #address-fields fragment
with region enabled and re-populated — no page reload and no custom JavaScript. When you render fields by
hand rather than with {{ form.region }}, honour the widget attributes the mixin sets (disabled,
autofocus, required).
What's included
Forms (django_hx_forms.forms)
- HtmxFormMixin — trigger/reset handling plus field-state helpers:
get_field_value(),enable_field()/disable_field()(and*_fieldsplurals),set_field_required(),set_field_queryset(),set_field_initial(),remove_field(),add_htmx_field_error(). - HtmxModelForm —
HtmxFormMixincombined with Django'sModelForm.
Views (django_hx_forms.views)
- HtmxFormUpdateViewMixin — renders the form partial on HTMX trigger requests; override
get_form_instance()for ModelForms. - IsHtmxRequestMixin — returns 403 for non-HTMX requests (used by
HtmxFormUpdateViewMixin).
Running the demo
A complete working example lives in demo/ — a shopping cart form where choosing a product type
reveals different fields, updates colour choices, and resets fields that no longer apply. It's a standalone
project that installs this package from the parent directory:
cd demo
uv sync
uv run python manage.py migrate
uv run python manage.py runserver
Then open http://127.0.0.1:8000/. See demo/README.md for a fuller walkthrough.
Development Setup
This project uses uv for dependency management.
# Install dependencies (including dev extras)
uv sync --extra dev
# Install pre-commit hooks
uv run pre-commit install
uv run pre-commit install --hook-type commit-msg
# Run tests
uv run pytest
# Run type checking
uv run mypy src
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_hx_forms-1.0.0.tar.gz.
File metadata
- Download URL: django_hx_forms-1.0.0.tar.gz
- Upload date:
- Size: 18.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1735e3a8e9a21c0877e56c943cad8a0e72b597d8a2230a7c053f5cac2c968ca8
|
|
| MD5 |
392c49a6df34c62bfefa91deb2f19315
|
|
| BLAKE2b-256 |
db2d76cb46abeff115dfbcea8601eccc565372a978e72818e4f94ec566919b3b
|
Provenance
The following attestation bundles were made for django_hx_forms-1.0.0.tar.gz:
Publisher:
publish.yml on howieweiner/django-hx-forms
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_hx_forms-1.0.0.tar.gz -
Subject digest:
1735e3a8e9a21c0877e56c943cad8a0e72b597d8a2230a7c053f5cac2c968ca8 - Sigstore transparency entry: 2058511923
- Sigstore integration time:
-
Permalink:
howieweiner/django-hx-forms@4445dd89e238543a9d8fd15d0fb0c06924c6a6fd -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/howieweiner
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@4445dd89e238543a9d8fd15d0fb0c06924c6a6fd -
Trigger Event:
release
-
Statement type:
File details
Details for the file django_hx_forms-1.0.0-py3-none-any.whl.
File metadata
- Download URL: django_hx_forms-1.0.0-py3-none-any.whl
- Upload date:
- Size: 11.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7f38d88100f107d3c001d6a3827130bbcab5c549c7c22b94b13fe77328d8526e
|
|
| MD5 |
4e04bf91a4037c5fdf8ac39ae12f38c4
|
|
| BLAKE2b-256 |
2448c7524e265d4f718cbd482a535f2dc58d48c2f0a17e340a3ff0c390649d14
|
Provenance
The following attestation bundles were made for django_hx_forms-1.0.0-py3-none-any.whl:
Publisher:
publish.yml on howieweiner/django-hx-forms
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_hx_forms-1.0.0-py3-none-any.whl -
Subject digest:
7f38d88100f107d3c001d6a3827130bbcab5c549c7c22b94b13fe77328d8526e - Sigstore transparency entry: 2058512508
- Sigstore integration time:
-
Permalink:
howieweiner/django-hx-forms@4445dd89e238543a9d8fd15d0fb0c06924c6a6fd -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/howieweiner
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@4445dd89e238543a9d8fd15d0fb0c06924c6a6fd -
Trigger Event:
release
-
Statement type: