Django Formsets Tutorial - Build dynamic forms with Htmx
Learn how to build dynamic forms with Django and Htmx.

This tutorial will cover how to build dynamic forms in Django using Htmx. It will also cover the basic concepts of Django formsets. But most of all, we're going to focus on how to make dynamic forms look and feel good.
You can find the code from this tutorial in this GitHub repository
If you want to watch the video instead of reading:
Project Setup
Start by creating a Django project:
virtualenv envsource env/bin/activatepip install djangodjango-admin startproject djforms
The latest version of Django at the time of this tutorial is 3.2.6
Run the migrations:
python manage.py migrate
Models
For this project we will work with the same set of models. Create a Django app and register it in the settings:
python manage.py startapp books
Add it to INSTALLED_APPS in settings.py Inside books/models.py add the following models:
And add the following to books/admin.py:
Using these models we can create an author and add as many books as we want to that author.
Run the migrations:
python manage.py makemigrationspython manage.py migrate
How to use Django Formsets
Formsets are one of the best parts of Django. The idea behind formsets is that you get a really flexible rendering of forms in your template and you don't have to write a lot of code to achieve it.
A formset is a layer of abstraction to work with multiple forms on the same page - Django docs
Formset factories are the main tools you can use to create formsets in Django:
from django.forms.models import ( inlineformset_factory, formset_factory, modelform_factory, modelformset_factory)
Create a file forms.py inside the books app and add the following:
We'll use the inlineformset_factory to create the formset but the other functions work pretty much the same way. The only difference is that modelform_factory and modelformset_factory work specifically with forms that inherit from forms.ModelForm.
Inside forms.py add the following:
Here we are creating an inline formset. The first argument is the parent model, which in this case is the Author. The second argument is the child model which is the Book. The form argument is the form used to create Book instances, and the other arguments change the styling of the form.
We'll now use this form in a function-based view. Inside books/views.py add the following:
In this view we create an instance of the BookFormSet and pass it into the context. If the request method is a POST request we then pass the request into the form, check if it is valid and then call the save() method. Because we are using a ModelForm this will save the values of the form as Book instances. Notice we're also assigning the instance of the formset as the author. The instance property is needed to link the child models to the parent.
Important to note is that this view requires the primary key of the author that we will add books to. Create a few authors in the Django admin:
python manage.py createsuperuser
Add a superuser so you can login to the admin:
Add the view to the project urls.py:
In the root of the project create a templates folder and inside it create create_book.html. Add the following to it:
Register the templates folder in the settings.py:
Visit http://127.0.0.1:8000/1 and you should see three forms to create books as well as the heading showing Create books for Joe.
Inspect the page and go to the Elements tab in the developer tools - you should see the following:
Django's formsets include a lot of hidden fields. The {{ formset.management_form }} renders them in the template. These fields are very important because they provide Django with meta information about the forms. To understand how to make dynamic formsets it is important to understand how the forms are rendered.
Create some books
Fill in the book form and submit it. You should see the newly created books display at the bottom of the page.
How to setup Htmx with Django
Htmx is a library that allows you to access modern browser features directly from HTML, rather than using JavaScript.
There are many examples of how to use Htmx for things like deleting table rows, progress bars, file uploads and much more.
So the question is; how do you use Htmx for dynamic forms?
There are some packages available to setup Htmx with Django. However, we are going to install it from scratch. You can also follow the official Htmx installation docs.
Create a base html file
We will use a base.html for all the other templates to inherit from so that they all contain the required files for Htmx. Create templates/base.html and add the following:
In the head of the document we've added the script to use the CDN for Htmx. We've also added a script at the bottom for Htmx to listen for requests and add the csrf_token so that POST requests are accepted.
Htmx Book Create View
The first Htmx view we'll create is the view that will return a new form.
Inside views.py add the following view:
Notice that we are using the BookForm here. Not the BookFormSet.
Add the view to the urls.py:
Now back inside create_book.html replace everything with the following:
We're now extending from base.html which lets us use Htmx properties. On the button element we've added the hx-get attribute which is pointing to the create-book-form URL. The target is set as the div with an ID of bookforms. Lastly the hx-swap attribute is for configuring how the response is rendered. beforeend will add the response to the end of the div.
When you click the button a GET request is sent to the backend where Django will return an HTML response of an empty BookForm. The HTML response is then added to the bookforms div.
Click the Add form button and you should see the following:
This gives a nice dynamic feeling.
To get the form submissions to work we have to change the create_book view. It no longer works with FormSets so it now looks like this:
Notice the else statement returns a render of the form with the book_form.html template so that the form errors can be displayed.
We also have to add some functionality to book_form.html
We have wrapped the form inside a div with two Htmx properties. The hx-target specifies this as the target which means it is pointing to itself. The hx-swap property has been set to outerHTML . Combining these two properties basically means that when the form is submitted, the entire form will be replaced by the response. The hx-post property on the button element ensures we send an Htmx request and not a normal request. The . value means the request will be sent to the current URL.
Test the form submission. You should see the form is replaced with success. That is because the HttpResponse is returning success. We can get more creative with this response by adding a detail view and returning the detail view response instead.
Htmx Book Detail View
Add the following view:
Add the view to the urls:
And create partials/book_detail.html:
Change the response in the create_book view from:
return HttpResponse("success")
to:
return redirect("detail-book", pk=book.id)
This will return the detail view of the book as the response for when the form is submitted.
Test the form submission and you should see the book title and number of pages being displayed, while the form disappears.
Htmx Book Update View
Now we have the create view and detail view working. We'll add the update view so that when the book is created we can click a button to edit that book.
Create the view:
This works similarly to the create view. The main difference is that we're passing in instance=book to the form to update the book. We're also returning partials/book_form.html which renders the same form as in the create_view. But be careful though. In the template there's no way to distinguish between updating books and creating new books.
Update book_form.html so that the button is different depending on if we're updating an existing book:
Add the view to the urls:
Replace the contents of book_detail.html with the following:
Similar to book_form.html , in this template we've added the attributes hx-target and hx-swap so that when the request is made it swaps the entire detail snippet for the response - which in this case is the populated form from the update view.
Test it out and check that the books are being updated after you save.
Htmx Book Delete View
Add the view:
Add it to the urls:
Add a delete button to the book_detail.html:
To make testing easier, loop through the books in the create_book.html. Add the following inside the content block:
Test the delete button. You should see the book removed from the page. Check the Django admin as well to confirm that the book is deleted.
Cancel Button
When clicking to update a book there is no way to cancel and go back to the detail view.
Update book_form.html to look like this:
We've added a button that requests the detail view. It will also replace the outer HTML with the response from the request. In this way it acts like a cancel button.
Now test to update a form and then click the cancel button. It should replace the form with the detail view of the book.
Django Formsets vs Htmx
Django's Formsets are very useful. But if you want to make the formsets look and feel good, particularly when using inline formsets, then you'll need to add JavaScript. This can land up being very complex and time consuming to get right.
I spent a lot of time trying to get formsets to play nice with Htmx. But ultimately decided that these two just don't work well together. Here's why:
Brennan Tymrak's article on dynamic formsets outlines a way to dynamically render formsets using JavaScript. When it comes to making formsets dynamic:
Adding additional forms requires using JavaScript to:
- Get an existing form from the page
- Make a copy of the form
- Increment the number of the form
- Insert the new form on the page
- Update the number of total forms in the management form
To try replicate this functionality in Htmx defeats the point of using Htmx. It requires some complicated logic that might as well be done using JavaScript.
Ultimately, the solution to achieving dynamic form logic with Htmx is to not use formsets. As you've seen in this tutorial so far we haven't used formsets at all when dealing with Htmx.
While this solution might not end up with exactly the result you were looking for, in my experience the things that matter are:
- How understandable and maintainable is the code?
- Does the desired outcome solve the problem?
With what we've shown so far I believe both these boxes can be ticked.
Making Forms Look Good
One of the issues with formsets is that while they function well, they normally don't look great.
We're going to add TailwindCSS to the project to style the forms. We'll use the CDN because it is easier to test with. In production you would want to minimise the size of the CSS bundle. A project like django-tailwind can help achieve this.
Add the CDN
To base.html add the CDN in the head tag:
In base.html wrap the content block like this:
Update create_book.html:
Update book_form.html:
Update book_detail.html:
Crispy Forms
The go-to package for better forms is django-crispy-forms. We're going to use the TailwindCSS template pack for styling.
Install both packages:
pip install django-crispy-forms crispy-tailwind
Configure the package in settings.py:
INSTALLED_APPS = ( ... "crispy_forms", "crispy_tailwind", ...)CRISPY_ALLOWED_TEMPLATE_PACKS = "tailwind"CRISPY_TEMPLATE_PACK = "tailwind"
Now in book_form.html load the tailwind filters at the top:
And make the form crispy:
{{ form|crispy }}
Now we have much better looking forms. Play around with the project. Maybe there are some areas you want to improve on.
Conclusion
So far Htmx has been very useful. Using it you can write simple code that significantly improves the UI experience. Dynamic forms feel like a breeze and we don't even have to work with formsets or JavaScript.
Django Pro
If you want to become a professional Django developer you can find many courses over on learn.justdjango.com.
Links: