Dynamically Add Forms in Django with Formsets and JavaScript
This tutorial demonstrates how multiple copies of a form can be dynamically added to a page and processed using Django formsets and JavaScript.
In a web app, a user may need to submit the same form multiple times in a row if they are inputting data to add objects to the database. Instead of having to submit the same form over and over, Django allows us to add mutliple copies of the same form to a web page using formsets.
We'll demonstrate this through a bird watching app to keep track of the birds a user has seen. It will include only two pages, a page with a list of birds and a page with a form to add birds to the list. You can see the completed source code for this example on GitHub.
For this tutorial, it would be helpful to have a basic understanding of:
- Django forms
- Django class based views
- JavaScript DOM manipulation
Setup
Model
The app will only have one model, Bird
, that stores information about each bird we've seen.
# models.py
from django.db import models
class Bird(models.Model):
common_name = models.CharField(max_length=250)
scientific_name = models.CharField(max_length=250)
def __str__(self):
return self.common_name
The model is composed of two CharFields
for the common name and the scientific name of the bird
URLs
We will manage URLs with a project level urls.py
file set to include the app level urls.py
file.
# project level urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('birds.urls')),
]
The app level urls.py
will include the paths to the two pages of the app.
# app level urls.py
from django.urls import path
from .views import BirdAddView, BirdListView
urlpatterns = [
path('add', BirdAddView.as_view(), name="add_bird"),
path('', BirdListView.as_view(), name="bird_list")
]
Views
Initially, we'll set up the view for just the page that lists the birds. This view will use a generic ListView
to get all bird instances and make them available for display in the specified template.
# views.py
from django.views.generic import ListView
from .models import Bird
class BirdListView(ListView):
model = Bird
template_name = "bird_list.html"
We will add the view for the form page later on.
Formsets
At this point, we could create a form and a view that would provide the form to the template so the user could add a new bird to the list. But the user would only be able to add one bird at a time. To add multiple birds at one time we need to use a formset instead of a regular form.
Formsets allow us to have multiple copies of the same form on a single page. This is helpful if the user should be able to submit multiple forms at once to create multiple model instances. In our example, this would allow the user to be able to add multiple birds to their list at one time without having to submit the form for each individual bird.
Model Formsets
Formsets can be created for different types of forms, including model forms. Since we want to create new instances of the Bird
model, a model formset is a natural fit.
If we just wanted to add a single model form to our page, we would create it like this:
# forms.py
from django.forms import ModelForm
from .models import Bird
# A regular form, not a formset
class BirdForm(ModelForm):
class Meta:
model = Bird
fields = [common_name, scientific_name]
The model form requires we specify the model we want the form to be associated with and the fields we want to include in the form.
To create a model formset, we don't need to define the model form at all. Instead, we use Django's modelformset_factory()
which returns a formset class for a given model.
# forms.py
from django.forms import modelformset_factory
from .models import Bird
BirdFormSet = modelformset_factory(
Bird, fields=("common_name", "scientific_name"), extra=1
)
modelformset_factory()
requires the first argument be the model the formset is for. After specifying the model, different optional arguments can be specified. We'll use two optional arguments - fields
and extra
.
fields
- specifies the fields to be displayed in the formextra
- the number of forms to initially display on the page
By setting extra
to 1, we will be passing only one form to the template initially. One is the default value, but we'll explicitly specify it for clarity. Setting extra
to any other number will provide that number of forms to the template.
Extra
allows us to display multiple copies of the form to the user, but we may not know how many birds the user wants to add at one time. If we show too few forms, then the user would still have to submit the form multiple times. If we show too many forms, the user may have to scroll far down the page to find the submit button. Luckily, by setting extra
we aren't limiting ourselves to only having that many copies of the form. We can add as many copies of the form as we realistically would need (up to 1000 copies) to our page even if extra
is set to 1. We can add the other copies of the form to the page dynamically with JavaScript. We'll do that when we create our template for displaying the form. Before we can do that, we need to create the view.
View
We need a view that can handle the GET request to initially display the form, and the POST request when the form in submitted. We'll use the class-based TemplateView
for the base view functionality and add methods for GET and POST requests.
The GET request requires that we create an instance of the formset and add it to the context.
# views.py
from django.views.generic import ListView, TemplateView # Import TemplateView
from .models import Bird
from .forms import BirdFormSet # Import the formset
class BirdListView(ListView):
model = Bird
template_name = "bird_list.html"
# View for adding birds
class BirdAddView(TemplateView):
template_name = "add_bird.html"
# Define method to handle GET request
def get(self, *args, **kwargs):
# Create an instance of the formset
formset = BirdFormSet(queryset=Bird.objects.none())
return self.render_to_response({'bird_formset': formset})
To create the formset instance, we first import the formset and then we call it in the get
method. If we just call the formset with no arguments, we will get a formset that contains a form for all Bird
instances in the database. Since we want this view to only add new birds we need to prevent the displayed forms from being pre-populated with Bird
instances. To do that we specific a custom queryset. We set the queryset argument to Bird.objects.none()
, which creates an empty queryset. This way no birds will be pre-populated in the forms.
Once we have created the formset instance, we call render_to_response
with the argument being a dictionary that has the formset assigned to the bird_formset
key.
For the POST request, we need to define a post
method that handles the form when it is submitted.
# views.py
from django.views.generic import ListView, TemplateView
from .models import Bird
from .forms import BirdFormSet
from django.urls import reverse_lazy
from django.shortcuts import redirect
class BirdListView(ListView):
model = Bird
template_name = "bird_list.html"
class BirdAddView(TemplateView):
template_name = "add_bird.html"
def get(self, *args, **kwargs):
formset = BirdFormSet(queryset=Bird.objects.none())
return self.render_to_response({'bird_formset': formset})
# Define method to handle POST request
def post(self, *args, **kwargs):
formset = BirdFormSet(data=self.request.POST)
# Check if submitted forms are valid
if formset.is_valid():
formset.save()
return redirect(reverse_lazy("bird_list"))
return self.render_to_response({'bird_formset': formset})
First, we create an instance of the BirdFormSet
. This time we include the submitted data from the request via self.request.POST
. Once the formset is created, we have to validate it. Similar to a regular form, is_valid()
can be called with a formset to validate the submitted forms and form fields. If the formset is valid, then we call save()
on the formset which creates new Bird
objects and adds them to the database. Once that is complete, we redirect the user to the page with the list of birds. If the formset is not valid, the formset is returned to the user with the appropriate error messages.
Template
Now that we have the views set up, we can create the templates that will be rendered to the user.
To display the list of birds, we will loop through object_list
which is provided by ListView
and includes all bird instances in the database.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bird List</title>
</head>
<body>
<h1>Bird List</h1>
<a href='{% url "add_bird" %}'>Add bird</a>
{% for bird in object_list %}
<p>{{bird.common_name}}: {{bird.scientific_name}}</p>
{% endfor %}
</body>
</html>
We'll display the common name and the scientific name for each bird. and include a link to the page with the form to add a bird to the list.
The template for our add bird page will initially just show the single form we specified with extra
.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add bird</title>
</head>
<body>
<h1>Add a new bird</h1>
<form id="form-container" method="POST">
{% csrf_token %}
{{bird_formset.management_form}}
{% for form in bird_formset %}
<div class="bird-form">
{{form.as_p}}
</div>
{% endfor %}
<button type="submit">Create Birds</button>
</form>
</body>
</html>
We create an HTML <form>
with an id of form-container
and method of POST
. Like a regular form, we include the csrf_token
. Unlike a regular form, we have to include {{bird_formset.management_form}}
. This inserts the management form into the page which includes hidden inputs that contain information about the number of forms being displayed and is used by Django when the form is submitted.
<input type="hidden" name="form-TOTAL_FORMS" value="1" id="id_form-TOTAL_FORMS">
<input type="hidden" name="form-INITIAL_FORMS" value="0" id="id_form-INITIAL_FORMS">
<input type="hidden" name="form-MIN_NUM_FORMS" value="0" id="id_form-MIN_NUM_FORMS">
<input type="hidden" name="form-MAX_NUM_FORMS" value="1000" id="id_form-MAX_NUM_FORMS">
The form-TOTAL_FORMS
input contains the value of the total number of forms being submitted. If it doesn't match the actual number of forms Django receives when the forms are submitted, an error will be raised.
Forms in the formset are added to the page using a for
loop to go through each form in the formset and render it. We've chosen to display the form in <p>
tags by using {{form.as_p}}
. The forms are rendered in HTML as:
<p>
<label for="id_form-0-common_name">Common name:</label>
<input type="text" name="form-0-common_name" maxlength="250" id="id_form-0-common_name">
</p>
<p>
<label for="id_form-0-scientific_name">Scientific name:</label>
<input type="text" name="form-0-scientific_name" maxlength="250" id="id_form-0-scientific_name">
<input type="hidden" name="form-0-id" id="id_form-0-id">
</p>
Each field in the form gets a name
and id
attribute that contains a number and the name of the field. The field for the common name has a name
attribute of form-0-common_name
and an id
of id_form-0-common_name
. These are important because the number in both of these attributes is used to identify what form the field is a part of. For our single form, each field is part of form 0, since form numbering starts at 0.
While this template will work and allow the user to submit one bird at a time, it still doesn't allow us to add more forms if the user wants to add more than one bird. We could have added more forms by setting extra
to a different number. Instead, we are going to use JavaScript to allow the user to choose how many forms they want to submit.
Making Formsets Dynamic
Since we are using formsets and have set up our views to handle a formset instead of a regular form, users can submit as many forms as they want at one time. We just need to make those forms available to them on the page. This can be done using DOM manipulation with JavaScript.
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
Before we get to JavaScript, we'll add a another button to the HTML form that will be used to add additional forms to the page.
<button id="add-form" type="button">Add Another Bird</button>
This button will be added directly before the "Create Birds" button.
To get the existing form on the page, we use document.querySelectorAll('.bird-form')
where bird-form
is the class given to the div
that contains the fields of each form. While document.querySelectorAll('.bird-form')
will return a list of all elements on the page that have a class of bird-form
, we only have one form on the page at this point, so it will return a list that includes only the one form. We will use document.querySelector()
to get the other elements on the page necessary to perform all the steps, namely the whole HTML form, the button the user can click to add a new form to the page, and the total forms input of the management form.
let birdForm = document.querySelectorAll(".bird-form")
let container = document.querySelector("#form-container")
let addButton = document.querySelector("#add-form")
let totalForms = document.querySelector("#id_form-TOTAL_FORMS")
We also need to get the number of the last form on the page. Again, since we know only one form will be initially displayed on the page, we could just say the last form is form 0. But in the case where we wanted more than one form on the page initially, we can find it by taking the number of forms stored in birdForm
and subtracting one to account for form numbering starting at 0.
let formNum = birdForm.length-1 // Get the number of the last form on the page with zero-based indexing
We only want a new form to be added to the page when the user clicks on the "Add Another Bird" button. This means the rest of the steps should only be executed when the button is pressed. We need to create a function that performs the remaining steps and attach it to the button press with an event listener. We'll call our function addForm
so it can be associated to the button click by
addButton.addEventListener('click', addForm)
The addForm
function will look like this
function addForm(e) {
e.preventDefault()
let newForm = birdForm[0].cloneNode(true) //Clone the bird form
let formRegex = RegExp(`form-(\\d){1}-`,'g') //Regex to find all instances of the form number
formNum++ //Increment the form number
newForm.innerHTML = newForm.innerHTML.replace(formRegex, `form-${formNum}-`) //Update the new form to have the correct form number
container.insertBefore(newForm, addButton) //Insert the new form at the end of the list of forms
totalForms.setAttribute('value', `${formNum+1}`) //Increment the number of total forms in the management form
}
First, we prevent the default action of the button click so only our addForm
function is executed. Then we create a new form by cloning birdForm
using .cloneNode()
. We pass true
as an argument so all child nodes of birdForm
are also copied to newForm
.
Since we created a copy, newForm
includes the exact same attribute values as the form already on the page. This means the form numbers are the same. Since we cannot submit two forms with the same number, we need to increment the number of the form. We do this by incrementing formNum
then using a regular expression to find and replace all instances of the form number in the HTML of newForm
.
By looking at the HTML of the form, we can see that all attributes which include the form number contain a common pattern of form-0-
. We'll create a regular expression to match this pattern and save it to formRegex
.
Next, we'll identify all instances that match formRegex
in the HTML of newForm
and replace it with a new string. We want the replacement string to be the same as the matched pattern expect the number will now be the value of the recently incremented formNum
. By accessing the HTML of newForm
with .innerHTML
and then using .replace()
, we are able to match the regular expression and perform the replacement.
Now that we have the correct form number in newForm
, we can add it to the page. We'll use .insertBefore()
and add the new form before addButton
.
With the form now on the page, we just need to make sure the management form is properly updated with the correct amount of forms that will be submitted. We need to increment the value of the form-TOTAL_FORMS
hidden field. We already saved this field in totalForms
. To update it, we'll use .setAttribute()
to set the value
attribute to one greater than the current form number. We do one greater because form-TOTAL_FORMS
includes the number of forms on the page starting at 1, while individual form numbering starts at 0.
We can now add all of the JavaScript to a <script>
tag before the closing <body>
tag to get our final template.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add bird</title>
</head>
<body>
<h1>Add a new bird</h1>
<form id="form-container" method="POST">
{% csrf_token %}
{{bird_formset.management_form}}
{% for form in bird_formset %}
<div class="bird-form">
{{form.as_p}}
</div>
{% endfor %}
<button id="add-form" type="button">Add Another Bird</button>
<button type="submit">Create Birds</button>
</form>
<script>
let birdForm = document.querySelectorAll(".bird-form")
let container = document.querySelector("#form-container")
let addButton = document.querySelector("#add-form")
let totalForms = document.querySelector("#id_form-TOTAL_FORMS")
let formNum = birdForm.length-1
addButton.addEventListener('click', addForm)
function addForm(e){
e.preventDefault()
let newForm = birdForm[0].cloneNode(true)
let formRegex = RegExp(`form-(\\d){1}-`,'g')
formNum++
newForm.innerHTML = newForm.innerHTML.replace(formRegex, `form-${formNum}-`)
container.insertBefore(newForm, addButton)
totalForms.setAttribute('value', `${formNum+1}`)
}
</script>
</body>
</html>
That's it! Now the user can add as many forms as they want to the page by clicking the "Add Another Bird" button and when the form is submitted they will be saved to the database, the user will be redirected back to the list of birds, and all of the newly submitted birds will be displayed.
Summary
Django formsets allow us to include multiple copies of the same form on a single page and correctly process them when submitted. Additionally, we can let the user choose how many forms they want to submit by using JavaScript to add more forms to the page. While this tutorial used modelformsets formsets for other types of forms can also be used.
Helpful Resources
Questions, comments, still trying to figure it out? - Let me know!