Django Forms in HTMX modals
The Django framework provides an excellent methodology for easily creating and managing forms while minimizing boilerplate code. Using this framework, developers can quickly leverage existing models or define custom form fields, and Django automatically generates an HTML form with all the validation included!
Typically, these forms are loaded as separate pages, but by harnessing the power of HTMX and bootstrap we can change that behaviour and load these forms inside the same page within UI modals, to give our users a much smoother experience. Accomplishing that goal will be focus of this article.
Setting up the Model and View
Let’s begin by creating a model and accompanying CBVs: list and update views. These are the views we will later enhance with HTMX modals.
First, the Django Model — I’ll reuse the same one from my previous article. For convenience I’m including the relevant portions here:
from django.db import models
class Category(models.Model):
name = models.CharField(max_length=64)
description = models.TextField(null=True, blank=True)
parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.PROTECT, related_name='children')Next, we create a simple ListView CBV for this model, whose job it is to display all records:
from django.views.generic import ListView
from .models import Category
class CategoryListView(ListView):
model = Category
template_name = 'category_list.html'
context_object_name = 'categories'The corresponding template for this CBV:
{% extends "layout.html" %}
{% block title %}Category List{% endblock %}
{% block content %}
<h1>All Categories</h1>
{% for category in categories %}
<div class="col-md-12 mt-2">
<a href="{% url "categories:update" category.id %}"
class="btn btn-sm btn-outline-primary">
Edit
</a>
{{ category }}
</div>
{% endfor %}
{% endblock %}Finally, we build an UpdateView CBV which can be used to edit a single record:
from django.views.generic import UpdateView
from .models import Category
class CategoryUpdateView(UpdateView):
model = Category
template_name = 'category_update.html'
fields = ['name', 'description', 'parent']And its matching template, below:
{% extends "layout.html" %}
{% load django_bootstrap5 %}
{% block title %}Edit Category{% endblock %}
{% block content %}
<h5>Edit Category: {{ category.name }}</h5>
<div id="update_form">
<form method="post" action="{% url 'categories:update' category.pk %}">
{% csrf_token %}
{% bootstrap_form form %}
<button type="submit" class="btn btn-primary">Update</button>
<a href="{% url 'recursive:index' %}" class="btn btn-secondary">Cancel</a>
</form>
</div>
{% endblock %}We now have our simple setup which displays all categories, and users can click the edit link to load a separate page with the form to edit that record. With all that in place, we will attempt to make the UpdateView CBV load inside of a UI modal instead of a separate page when the user clicks the “edit” button next to a category record.
Installing HTMX in Django
The first step we’ll need to take is to install the HTMX framework into our Django project, so our apps can utilize it. Luckily, since this is purely a client-side technology, all we need to do is to update our base template’s <head> element to load the HTMX library in a <script> element, seen below:
<html lang="en">
<head>
<meta charset="utf-8">
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
</head>
<body>
{% block contents %}
{% endblock %}
</body>
</html>The example above uses the latest version of HTMX available at the time of writing. Check the official HTMX docs for the latest version to include in your project.
Next, let’s put this framework to work.
Creating a UI modal
For those unfamiliar with the term, a modal (with an “a”, not to be confused with Django models with an “e”) is a UI component on the front-end which creates a floating element which appears on top of the rest of the UI, covering it up — a bit like a pop-up window — in order to call attention to it.
As the saying goes, “a picture is worth a thousand words” so let’s visualize it by creating a simple example on our template. We start by creating a new sub-template, modal.html and in it we’ll leverage the Bootstrap UI framework to make our modal, below:
<div class="modal modal-lg fade" id="update_modal" tabindex="-1" aria-labelledby="updateModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="updateModalLabel">Sample Modal Title</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
This is sample text for our modal.
</div>
</div>
</div>
</div>In order to activate the modal, we create a button that is tied to the modal:
<button type="button" class="btn btn-lg btn-primary" data-bs-toggle="modal" data-bs-target="#update_modal">
Test modal
</button>The button is now connected to the modal via the value in the data-bs-target attribute, which uses a CSS-like selector to point at our modal which we assigned the ID of update_modal.
So far, we have a Django UpdateView CBV, and an empty UI modal. Now it’s time to use HTMX to put them together!
Using HTMX to load the form
HTMX is a wonderfully simple tool. At its core, all it does is make an HTTP call to the server, collect the HTML, then dynamically places it into the current document at the desired location.
In our case, we’re going to tell HTMX to load the form generated by our UpdateView CBV, place that form inside of the modal, then show the modal.
HTMX introduces some new attributes to our HTML entities. The ones we will familiarize ourselves with first for this example are the following:
hx-getrepresents how we want fetched from the server. This will be provided in the form of a URL to the remote page whose contents we want to grab. (hx-post, etc can be used for other HTTP verbs)hx-selectlimits the contents returned from the server to a specific subset. This takes the form of a CSS-like selector to identify which element (and all its descendants) will be included. If this attribute is omitted, the entire document is processed.hx-targettells HTMX where to place the contents of the remote page onto the current page. Likehx-selectthis will also be a CSS-like selector, though this time it will uniquely identify where on the current page to place the remote content.hx-swapis what determines how to insert the remote content into the desired location:innerHTMLwill replace the entire contents of thehx-targetelement, whereasbeforeendwill append the contents to the end of the inside of the element. Consult the docs for a complete list of possible options for this attribute.hx-triggeris when the HTMX action is initiated. The most common values here areloadfor when the current page finishes loading, andclickfor when the current element is clicked. Consult the docs for all possible options.
Given this information, we can return to our categories_list.html template and replace our existing “edit” hyperlink with a button that combines the attributes which displays our modal, with the HTMX attributes above. Combined together, our new “edit” button now fetches the UpdateView CBV form for the category in question and places it inside the modal:
{% extends "base.html" %}
{% block content %}
{% include "modal.html" %}
<h1>All Categories</h1>
{% for category in categories %}
<div class="col-md-12 mt-2">
<button type="button"
class="btn btn-sm btn-outline-primary"
hx-get="{% url "update_category" category.id %}"
hx-target="#update_modal .modal-body"
hx-select="#update_form"
hx-swap="innerHTML"
hx-trigger="click"
data-bs-toggle="modal"
data-bs-target="#update_modal">
Edit
</button>
{{ category }}
</div>
{% endfor %}
{% endblock %}The hx-* attributes is what controls the HTMX behaviour for this element, while the data-bs-* attributes control Bootstrap 5’s behaviour for displaying the modal on the page. (Note that Bootstrap 4 uses slightly different attribute names)
This is what our modal now looks like when the button is pressed:
The option to browse to the full-page non-modal version of the UpdateView still exists for any category by simply browsing directly to the page in question.
Dynamically Updating the Modal Title
Currently, our modal keeps the same boring title: “Edit Category” no matter which category we are updating. Let’s spice this up by dynamically replacing the modal title with the name of the category the user is editing!
To accomplish this, we’ll make use of HTMX’s out-of-band swap feature. The way it works is very simple: the server-side template needs to set both the id and the hx-oob-swap attributes set for any elements that will be swapped, and the client page needs to have an element with the same ID present on the page. This is what indicates to HTMX which elements to automatically replace on the current page with the matching one from the server.
To make this work in our app, we only need to change one thing: on our category_update.html template, we add the id and hx-swap-oob attributes to our h5 element. Here is what it now looks like:
<h5 id="updateModalLabel" hx-swap-oob="true">Edit Category: {{ category.name }}</h5>Note that the id attribute we gave it matches the exact id of the modal’s title element. Now, whenever we use the “edit” button to bring up the edit modal, its title is automatically updated with the name of the category record we are currently editing:
Integrating the HTMX framework into our Django project opens up a whole world of new possibilities. The strength of this pairing is that it adds very little complexity to the code base and can be dropped-in with ease. It leverages all our existing CBVs in our project without having to redesign or re-architect any of them.
As with most tools I like to showcase in my articles, this example barely scratches the surface of what this framework is capable of. I highly encourage readers to dive deeper into the HTMX docs to learn more about what it can accomplish.
Have you found other interesting ways to incorporate HTMX into your Django project? Share your coolest integrations in the comments below!
