Horilla CRM uses HTMX for partial page updates while keeping Django server-rendered templates. The shell loads once; #mainContent and #modalBox swap HTML fragments on each interaction.

This post is Part 5 of 28 in the Horilla CRM Technical Blog series.

What is HTMX in Horilla CRM?

HTMX in Horilla CRM enables you to:

  • MAIN_CONTENT_HX_ATTRS for sidebar navigation
  • List → detail transitions without full reload
  • Modals for create/edit via #modalBox
  • htmx_required on fragment-only endpoints
  • OOB swaps for settings sidebar highlights

The page shell: stable regions

The main layout defines persistent DOM targets:

Target IDPurpose
#mainContentPrimary work area (list, detail, settings pages)
#modalBoxCreate/edit modals
#settings-contentSettings module inner panel
#settings-sidebarSettings navigation (OOB swaps)

The shell loads once; inner regions swap via HTMX.

Pattern 1: In-app navigation with MAIN_CONTENT_HX_ATTRS

From horilla/menu/__init__.py:

MAIN_CONTENT_HX_ATTRS = {
    "hx-boost": "true",
    "hx-target": "#mainContent",
    "hx-select": "#mainContent",
    "hx-swap": "outerHTML",
}

Apply to sidebar links:

# menu.py sub-section item
{
    "label": _("Leads"),
    "url": reverse_lazy("leads:leads_view"),
    **MAIN_CONTENT_HX_ATTRS,
    "perm": "leads.view_lead",
}

What happens: Clicking “Leads” fetches the full HTML response, extracts #mainContent from it, and replaces the current main region. The sidebar/header stays mounted — no full reload.

hx-push-url=”true” on detail links updates the browser URL for bookmarking.

Pattern 2: List → detail without leaving the shell

From LeadListView.col_attrs:

"title": {
    "hx-get": "{get_detail_url}",
    "hx-target": "#mainContent",
    "hx-swap": "outerHTML",
    "hx-push-url": "true",
    "hx-select": "#mainContent",
    "permission": "leads.view_lead",
    "own_permission": "leads.view_own_lead",
    "owner_field": "lead_owner",
}

{get_detail_url} resolves to the model’s get_detail_url() method. Generic list templates expand these placeholders per row.

Pattern 3: Modals for create/edit

Floating menu create actions target the modal:

@floating_menu.register
class LeadFloating:
    items = {
        "hx-target": "#modalBox",
        "hx-swap": "innerHTML",
        "onclick": "openModal()",
        "perm": ["leads.add_lead"],
    }

Form views return partial templates that fit inside #modalBox. On success, HTMX triggers may refresh #mainContent or close the modal via HX-Trigger headers.

Pattern 4: htmx_required on partial endpoints

Many list/kanban/nav views require HTMX so they cannot be bookmarked as bare HTML fragments:

from horilla.utils.decorators import htmx_required
 
@method_decorator(htmx_required, name="dispatch")
class LeadListView(LoginRequiredMixin, HorillaListView):
    ...

Direct browser navigation to /crm/leads/leads-list/ may redirect or 400; users enter through the shell view (leads-view/) which loads chrome then HX-gets the list.

Why: Prevents broken layouts when users open fragment URLs in new tabs without the shell (some views allow fallback — check per module).

Pattern 5: Settings with out-of-band (OOB) swaps

Settings menu items often update two regions at once:

{
    "label": _("Assignment Rules"),
    "url": reverse_lazy("leads:leads_assignment_view"),
    "hx-target": "#settings-content",
    "hx-select": "#lead-assignment-view",
    "hx-select-oob": "#settings-sidebar",
    "perm": "leads.view_leadassignmentrule",
}

hx-select-oob replaces #settings-sidebar from the same response — highlight active section without a second request.

Pattern 6: Activity tab — nested HTMX

The activity module (horilla/contrib/activity/templates/activity_tab.html) uses HTMX for:

  • Filtering activities by type
  • Inline task completion
  • Loading note/meeting forms into sub-panels

Typical attributes you’ll see:

<button hx-get="{% url 'activity:task_toggle' pk=task.id %}"
        hx-target="#activity-list-{{ object.pk }}"
        hx-swap="innerHTML">

Detail record tabs load activity via hx-get on tab click — deferring heavy queries until the user opens the tab.

Pattern 7: View mode switching (list/kanban/card)

The nav bar (HorillaNavView) renders toggle buttons that HX-get different endpoints into #mainContent:

leads-list/   → HorillaListView
leads-kanban/ → HorillaKanbanView
leads-card/   → HorillaCardView

Shared filterset_class keeps filter state consistent across modes (query string preserved).

HTMX + permissions

Template tags and generic mixins strip hx-* attributes when the user lacks permission — buttons disappear rather than failing at click time.

Always pair HTMX endpoints with the same decorators as full-page views:

@permission_required_or_denied(["leads.view_lead", "leads.view_own_lead"])

HTMX + real-time (Channels)

Notifications use WebSockets (horilla_notifications) alongside HTMX — not instead of it.

Typical flow:

  1. Server event → push notification JSON over WebSocket
  2. Client JS updates badge/toast
  3. User clicks → HTMX loads detail into #mainContent

Keep domain logic on the server; use WS for ephemeral alerts.

Frontend stack (no SPA)

LayerChoice
CSSTailwind + Flowbite
ChartsECharts
Rich textSummernote
Select FKSelect2 AJAX (/generics/…/select2/)
InteractivityHTMX + small vanilla JS helpers

AppLauncher registers per-app JS via js_files (Part 2) for module-specific behavior.

Debugging tips

  1. Network tab — HTMX requests include HX-Request: true header
  2. Wrong fragment — check hx-select matches an ID in the response template
  3. Full page in #mainContent — view template may extend wrong base; partials should extend a minimal base or use {% block %} only
  4. 403 on HX-get — permission decorator or missing CSRF on POST
  5. Stale sidebar — add hx-select-oob for secondary regions

Benefits of HTMX in Horilla CRM

  • SPA-like speed without a JavaScript framework
  • Easier debugging — HTML over the wire
  • Smaller frontend bundles
  • Permissions strip hx-* attrs for unauthorized users
  • Works with WebSocket notifications for alerts

HTMX is central to Horilla CRM’s UX. Master #mainContent, modal targets, and htmx_required — then every module feels fast and consistent.

Continue the series

Previous: Part 4 — How Horilla CRM’s Four-Layer Permission System Enhances Data Security

Next: Part 6 — How to Extend Horilla CRM Without Forking the Core Framework

More posts are at Horilla Blogs; share feedback on GitHub.

Share this article