Views & Templates¶
MedUX uses Django class-based views with HTMX for server-driven interactivity. This page documents the common patterns.
Base Templates¶
All pages extend Conjunto's base template:
{% extends "conjunto/base.html" %}
{% load i18n gdaps conjunto %}
Conjunto provides the full layout with topbar, sidebar, offcanvas, and statusbar regions.
HTMX Patterns¶
CSRF Handling¶
CSRF tokens are included globally via the body_attrs block in the base
template:
{% load django_htmx %}
{% block body_attrs %}hx-headers='{"x-csrftoken": "{{ csrf_token }}"}'{% endblock %}
HTMX Inheritance is Disabled¶
Conjunto ships with:
<meta name="htmx-config" content='{"disableInheritance": true}'>
This means every element that issues an HTMX request must declare
its own hx-get/hx-post, hx-target, hx-swap, etc. Attributes
are never inherited from parent elements. This prevents subtle bugs
from ancestor attributes leaking into nested elements.
Form Views¶
Use HtmxFormViewMixin for forms that should work with HTMX partial
swaps:
from conjunto.views import HtmxFormViewMixin
from django.views.generic import FormView
class MyFormView(HtmxFormViewMixin, FormView):
template_name = "myapp/my_form.html"
form_class = MyForm
Administration List Views¶
Admin list pages (entries rendered under the /adm/ sidebar) inherit
from medux.common.views.AdministrationListView, which combines
AdministrationPageMixin (permissions + admin sidebar) with
conjunto.views.TableListView (search, filters, header actions,
django-tables2 body). See the conjunto docs for the full
TableListView surface.
from medux.common.views import AdministrationListView
from myapp.tables import MyObjectTable
from conjunto.filters import BooleanFilter
from conjunto.tables import standard_add_action
class MyObjectListView(AdministrationListView):
model = MyObject
table_class = MyObjectTable
permission_required = "myapp.view_myobject"
search_fields = ("name", "email")
filters = [BooleanFilter("is_active")]
header_actions = [standard_add_action("adm:myapp:myobject")]
The default template is common/administration_list.html, which
delegates to conjunto/tables/list_card.html. For legacy templates
the view still exposes object_add_url / add_title in context.
Modal Pattern¶
Modal dialogs are a core UI pattern in MedUX. The flow is fully managed by Conjunto's JavaScript — no Bootstrap data API attributes needed.
Trigger Button¶
Every button that opens a modal carries exactly three HTMX attributes:
<button type="button"
class="btn btn-primary"
hx-get="{% url 'myplugin:thing-create' %}"
hx-target="#dialog"
hx-swap="innerHTML">
{% trans "Add thing" %}
</button>
The project-wide modal target is #dialog (the element lives in
conjunto/base.html). The legacy cj--prefixed id has been dropped
from conjunto — always use #dialog.
Modal Lifecycle¶
- Click sends a
GETto the modal view - Response (rendered
conjunto/modal_form.html) is swapped into#dialog - Conjunto's JS detects the swap and shows the Bootstrap modal
- On submit, the form
POSTs back. On success:204 No Content - Conjunto's JS hides the modal and cleans up
#dialog
Modal Forms¶
Forms used inside modals must set helper.form_tag = False to avoid
nested <form> tags:
from crispy_forms.helper import FormHelper
class MyModalForm(forms.Form):
name = forms.CharField()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper(self)
self.helper.form_tag = False # required for modals
Warning
Without form_tag = False, crispy renders a second <form> inside
the modal's outer form. The browser silently closes the outer form,
resulting in an empty-looking modal with no visible error.
Success Events¶
Modal forms return 204 No Content with an HX-Trigger header to
notify the page:
def form_valid(self, form):
form.save()
return HttpResponse(status=204, headers={
"HX-Trigger": "myapp:thing:changed",
})
List views can self-refresh on this event:
<div id="thing-table"
hx-get="{{ request.path }}"
hx-trigger="myapp:thing:changed from:body"
hx-swap="outerHTML"
hx-select="#thing-table">
...
</div>
Anti-Pattern: Bootstrap Data API¶
Do not add data-bs-toggle="modal" or data-bs-target="#modal" to
trigger buttons. This causes the modal to open immediately on click
before the HTMX response arrives, resulting in an empty dialog flash.
Conjunto's JS handles the full lifecycle.
Toast Notifications¶
Show a toast notification via the HX-Trigger header:
import json
from django.http import HttpResponse
response = HttpResponse()
response["HX-Trigger"] = json.dumps({
"showToast": {"message": "Saved!", "type": "success"}
})
return response
Toast types: success, warning, error, info.
Template Tags¶
Load the standard template tags at the top of every template:
{% load i18n gdaps conjunto %}
Breadcrumbs¶
{% block breadcrumbs %}
{% breadcrumb "Home" "home" %}
{% endblock %}
Override in child templates with {{ block.super }}:
{% block breadcrumbs %}
{{ block.super }}
{% breadcrumb page_title "person:detail" %}
{% endblock %}