Open In App

Add Forms Dynamically In Django Using Formset And JavaScript

Last Updated : 29 Aug, 2024
Comments
Improve
Suggest changes
Like Article
Like
Report

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.

Adding a Form to a Django Formset Dynamically

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',
]

Formset for a Normal Form

Define the Form

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>

Management Form:

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.

Configure URLs

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


Formset for a Model Form

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

Define the Model Form and Formset

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

View to Handle Model FormSet

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


Formset with Both Forms and Model Forms

Define the Normal Form and Model Form

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.


Next Article
Article Tags :
Practice Tags :

Similar Reads