Skip to content

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 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.

  1. Click sends a GET to the modal view
  2. Response (rendered conjunto/modal_form.html) is swapped into #dialog
  3. Conjunto's JS detects the swap and shows the Bootstrap modal
  4. On submit, the form POSTs back. On success: 204 No Content
  5. Conjunto's JS hides the modal and cleans up #dialog

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 %}
{% block breadcrumbs %}
  {% breadcrumb "Home" "home" %}
{% endblock %}

Override in child templates with {{ block.super }}:

{% block breadcrumbs %}
  {{ block.super }}
  {% breadcrumb page_title "person:detail" %}
{% endblock %}