Add Forms Dynamically In Django Using Formset And JavaScript
Last Updated :
29 Aug, 2024
Formsets in Django allow us to manage a collection of forms as a single unit. They are particularly useful when we need to handle multiple forms on a single page, like adding or editing multiple objects at once. However, a common requirement in many applications is the ability to add forms to a formset without a page reloading dynamically.
In this article, we’ll explore how to implement formsets in a Django project with a to-do list application, covering two use cases:
- Formset for a Normal Form
- Formset for a Model Form
Prerequisites: To follow along with this tutorial, we should have a basic understanding of Django forms and formsets, as well as some familiarity with JavaScript and jQuery.
We'll create a Django project named "GFGTodo" that allows us to manage a to-do list using formsets. This project will include dynamic form functionality for both normal and model forms.
Set Up the Django Project
Create a New Django Project
django-admin startproject GFGTodo
cd GFGTodo
Create a New Django App
python manage.py startapp todo
Edit GFGTodo/settings.py and add 'todo' to the INSTALLED_APPS list:
INSTALLED_APPS = [
# other apps
'todo',
]
Now, let's create a form for the Task
and set up a formset. Create a file named forms.py in the todo directory.
Here, we use the formset_factory
function is used to create a formset from a standard Django form.
Python
# todo/forms.py
from django import forms
from django.forms import formset_factory
class TaskForm(forms.Form):
name = forms.CharField(max_length=100, label='Task Name')
description = forms.CharField(widget=forms.Textarea, label='Description')
TaskFormSet = formset_factory(TaskForm, extra=1)
The number of forms rendered initially is controlled by the extra argument. The above formset will have a single form initially.
Create Views
In the view, we'll render the formset and handle the form submission. Edit views.py in the todo app:
Python
# todo/views.py
from django.shortcuts import render
from .forms import TaskFormSet
def task_list(request):
if request.method == 'POST':
formset = TaskFormSet(request.POST)
if formset.is_valid():
# Handle formset data
pass
else:
formset = TaskFormSet()
return render(request, 'todo/task_list.html', {'formset': formset})
After validating the form data we can play with the data submitted.
Create Templates
In the template, we'll render the formset and add the necessary HTML and JavaScript to dynamically add new forms. Create templates/todo/task_list.html:
HTML
<!DOCTYPE html>
<html>
<head>
<title>Task List</title>
<!-- Load jQuery from a CDN for handling dynamic behaviors -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<h1>Task List</h1>
<!-- Form to submit the formset data -->
<form method="post">
{% csrf_token %}
<!-- Management form, necessary for formset to track the number of forms -->
{{ formset.management_form }}
<!-- Container to hold all the form instances -->
<div id="formset">
{% for form in formset %}
<div class="form-row">
{{ form.as_p }}
<button type="button" class="remove-form">Remove</button>
</div>
{% endfor %}
</div>
<!-- Button to dynamically add a new form to the formset -->
<button type="button" id="add-form">Add Form</button>
<button type="submit">Save</button>
</form>
<script>
$(document).ready(function() {
// Initialize form index to the last form index in the formset
let formIdx = {{ formset.total_form_count|add:"-1" }};
// Click event handler to add a new form
$('#add-form').click(function() {
$('#formset').append($('#formset .form-row:last').clone().find('input').each(function() {
// Update the name attribute of the cloned form fields with the new form index
let name = $(this).attr('name').replace(/-\d+-/, '-' + formIdx + '-');
// Clear the value of the input field
$(this).attr('name', name).val('');
}).end().find('input[name$=-id]').val('').end().find('.remove-form').show().end());
formIdx++; // Increment the form index
// Update the management form's TOTAL_FORMS field
$('#id_form-TOTAL_FORMS').val(formIdx);
});
// Click event handler to remove a form
$('#formset').on('click', '.remove-form', function() {
// Remove the selected form row
$(this).closest('.form-row').remove();
formIdx--; // Decrement the form index
// Update the management form's TOTAL_FORMS field
$('#id_form-TOTAL_FORMS').val(formIdx);
});
// Initially hide the remove button for the existing forms
$('.remove-form').hide();
});
</script>
</body>
</html>
The {{ formset.management_form }}
is essential for Django to manage the number of forms in the formset correctly. It tracks how many forms are being sent back to the server.
Explanation of the Script:
- Dynamic Form Addition:
- When the "Add Form" button is clicked, the last form in the formset is cloned.
- The `name` attributes of the input fields in the cloned form are updated to reflect the new form index.
- The input fields are cleared to ensure the new form is blank.
- The total number of forms (`TOTAL_FORMS`) is updated to include the newly added form.
- Form Removal:
- Each form has a "Remove" button that, when clicked, removes the form from the DOM.
- The form index is decremented, and the `TOTAL_FORMS` field is updated accordingly.
- Initial Form Setup:
- The "Remove" button is hidden for the initial forms, assuming we don't want to allow the user to remove them by default. This can be customized based on our needs.
Add a URL pattern in todo/urls.py:
Python
# todo/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.task_list, name='task_list'),
]
Include this URL configuration in GFGTodo/urls.py:
Python
# GFGTodo/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path('tasks/', include('todo.urls')),
]
Run the Server
python manage.py runserver
Output
Define the Model
We'll create a simple model representing an Task in an Todo List: Edit models.py in the todo app:
Python
# todo/models.py
from django.db import models
class Task(models.Model):
name = models.CharField(max_length=100)
description = models.TextField()
def __str__(self):
return self.name
Here, we will use modelformset_factory for model-based forms, automatically handling form creation, validation, and saving of instances to the database. Update forms.py:
Python
# todo/forms.py
from django.forms import modelformset_factory
from .models import Task
TaskFormSet = modelformset_factory(Task, fields=('name', 'description'), extra=1)
Migrate the Database
Run the migrations to create the Task model table:
python manage.py makemigrations
python manage.py migrate
Here, we are updating the same view to handle the model formset. Update views.py:
Python
# todo/views.py
from django.shortcuts import render
from .forms import TaskFormSet
def task_list(request):
if request.method == 'POST':
formset = TaskFormSet(request.POST, queryset=Task.objects.all())
if formset.is_valid():
formset.save()
else:
formset = TaskFormSet(queryset=Task.objects.all())
return render(request, 'todo/task_list.html', {'formset': formset})
As the form is a model form, after performing the validation we call the save method to create Tasks.
Run the Server
python manage.py runserver
Output
Update forms.py to include both normal forms and model forms:
Python
# todo/forms.py
from django import forms
from django.forms import formset_factory, modelformset_factory
from .models import Task
class TaskForm(forms.Form):
name = forms.CharField(max_length=100, label='Task Name')
description = forms.CharField(widget=forms.Textarea, label='Description')
class TaskModelForm(forms.ModelForm):
class Meta:
model = Task
fields = ['name', 'description']
TaskFormSet = formset_factory(TaskForm, extra=1)
TaskModelFormSet = modelformset_factory(Task, form=TaskModelForm, extra=1)
Update the Views
Modify views.py to handle both formsets:
Python
# todo/views.py
from django.shortcuts import render
from .forms import TaskFormSet, TaskModelFormSet
def task_list(request):
if request.method == 'POST':
formset = TaskFormSet(request.POST)
model_formset = TaskModelFormSet(request.POST)
if formset.is_valid() and model_formset.is_valid():
# Process normal formset data
# Process model formset data
formset.save()
model_formset.save()
else:
formset = TaskFormSet()
model_formset = TaskModelFormSet(queryset=Task.objects.all())
return render(request, 'todo/task_list.html', {'formset': formset, 'model_formset': model_formset})
Update Templates
Update task_list.html to include both formsets:
HTML
<!DOCTYPE html>
<html>
<head>
<title>Task List</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<h1>Task List</h1>
<h2>Normal Formset</h2>
<form method="post" id="normal-formset">
{% csrf_token %}
{{ formset.management_form }}
<div id="formset">
{% for form in formset %}
<div class="form-row">
{{ form.as_p }}
<button type="button" class="remove-form">Remove</button>
</div>
{% endfor %}
</div>
<button type="button" id="add-form">Add Form</button>
<button type="submit">Save Normal Forms</button>
</form>
<h2>Model Formset</h2>
<form method="post" id="model-formset">
{% csrf_token %}
{{ model_formset.management_form }}
<div id="model_formset">
{% for form in model_formset %}
<div class="form-row">
{{ form.as_p }}
<button type="button" class="remove-form">Remove</button>
</div>
{% endfor %}
</div>
<button type="button" id="add-model-form">Add Model Form</button>
<button type="submit">Save Model Forms</button>
</form>
<script>
$(document).ready(function() {
// Handling Normal Formset
let formIdx = {{ formset.total_form_count|add:"-1" }};
$('#add-form').click(function() {
$('#formset').append($('#formset .form-row:last').clone().find('input').each(function() {
let name = $(this).attr('name').replace(/-\d+-/, '-' + formIdx + '-');
$(this).attr('name', name).val('');
}).end().find('input[name$=-id]').val('').end().find('.remove-form').show().end());
formIdx++;
$('#id_form-TOTAL_FORMS').val(formIdx);
});
$('#formset').on('click', '.remove-form', function() {
$(this).closest('.form-row').remove();
formIdx--;
$('#id_form-TOTAL_FORMS').val(formIdx);
});
$('.remove-form').hide(); // Hide remove button for initial forms
// Handling Model Formset
let modelFormIdx = {{ model_formset.total_form_count|add:"-1" }};
$('#add-model-form').click(function() {
$('#model_formset').append($('#model_formset .form-row:last').clone().find('input').each(function() {
let name = $(this).attr('name').replace(/-\d+-/, '-' + modelFormIdx + '-');
$(this).attr('name', name).val('');
}).end().find('input[name$=-id]').val('').end().find('.remove-form').show().end());
modelFormIdx++;
$('#id_form-TOTAL_FORMS').val(modelFormIdx);
});
$('#model_formset').on('click', '.remove-form', function() {
$(this).closest('.form-row').remove();
modelFormIdx--;
$('#id_form-TOTAL_FORMS').val(modelFormIdx);
});
$('.remove-form').hide(); // Hide remove button for initial forms
});
</script>
</body>
</html>
Run the Server
python manage.py runserver
Output
Conclusion
By following this article, we have learned how to dynamically add forms to a Django formset in three different scenarios:
- Normal Forms: Allows users to manage multiple instances of a non-model form.
- Model Forms: Enables users to manage multiple instances of a model form tied to a database.
- Combination of Both: Handles both normal forms and model forms in a single view.
This approach not only enhances user experience but also streamlines form management in complex applications. Feel free to customize and expand upon this basic setup to fit your specific needs.