Sitemap
Press enter or click to view image in full size
Photo by Henry Lim on Unsplash

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-get represents 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-select limits 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-target tells HTMX where to place the contents of the remote page onto the current page. Like hx-select this 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-swap is what determines how to insert the remote content into the desired location: innerHTML will replace the entire contents of the hx-target element, whereas beforeend will 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-trigger is when the HTMX action is initiated. The most common values here are load for when the current page finishes loading, and click for 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:

Press enter or click to view image in full size
“Edit Category” modal appears after clicking the “edit” button for the first record

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.

Press enter or click to view image in full size
The “Edit Category” page outside of the modal

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:

Press enter or click to view image in full size
The “Edit Category” modal again, this time the modal header is updated to the category’s name

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!

Recommended Reading

--

--