Dynamically Add Form to Formset Using JavaScript and Django

Screenshot of the example Django app for dynamically adding forms.
Sample Django project illustrating dynamic formset addition

Recently, I created a form (in Django 1.3) where users could add extra rows of form fields as required, similar to how you can add extra reminders in Google Calendar. Patternry calls it an inline input adder. I found it a bit tricky to achieve this, so I thought I’d write about it.

Note: This is only a ‘one-way’ form. That is, you add further extra rows/items and submit the form. You can’t view or edit previously entered rows/items in the form again .


Update, 12 Aug, 2012: Here’s a better, work-in-progress code which you might find useful: dynamicformtodo on GitHub


There are two main steps:

  1. Use JavaScript to dynamically create additional forms (on the client side).
  2. Capture submitted forms on the server side.

Let’s deal with the second part first, because I found this simpler to implement. I’m going to be using a simple todo list application as an example. You can download the example project here.

Server Side Splendour

Django Formsets

Django has the concept of Formsets that help when dealing with multiple identical forms on the same page. This will make capturing submitted forms a cinch.

The Models (models.py)

models.py [View on GitHub]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# models.py
from django.db import models


class TodoList(models.Model):
    name = models.CharField(max_length=100)

    def __unicode__(self):
        return self.name


class TodoItem(models.Model):
    name = models.CharField(max_length=150,
               help_text="e.g. Buy milk, wash dog etc")
    list = models.ForeignKey(TodoList)

    def __unicode__(self):
        return self.name + " (" + str(self.list) + ")"

Straightforward stuff. And you have a form for each model:

The Forms (forms.py)

forms.py [View on Github]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# forms.py
# Change as necessary
from dynamicform.todo.models import *
from django.forms import ModelForm


class TodoListForm(ModelForm):
  class Meta:
    model = TodoList


class TodoItemForm(ModelForm):
  class Meta:
    model = TodoItem
    exclude = ('list',)

The View (views.py)

views.py [View on Github]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# views.py
from django.shortcuts import render_to_response
from django.core.context_processors import csrf
from django.template import RequestContext  # For CSRF
from django.forms.formsets import formset_factory, BaseFormSet
from django.http import HttpResponse, HttpResponseRedirect
from dynamicform.todo.forms import *


def index(request):
    # This class is used to make empty formset forms required
    # See http://stackoverflow.com/questions/2406537/django-formsets-make-first-required/4951032#4951032
    class RequiredFormSet(BaseFormSet):
        def __init__(self, *args, **kwargs):
            super(RequiredFormSet, self).__init__(*args, **kwargs)
            for form in self.forms:
                form.empty_permitted = False
    TodoItemFormSet = formset_factory(TodoItemForm, max_num=10, formset=RequiredFormSet)
    if request.method == 'POST': # If the form has been submitted...
        todo_list_form = TodoListForm(request.POST) # A form bound to the POST data
        # Create a formset from the submitted data
        todo_item_formset = TodoItemFormSet(request.POST, request.FILES)

        if todo_list_form.is_valid() and todo_item_formset.is_valid():
            todo_list = todo_list_form.save()
            for form in todo_item_formset.forms:
                todo_item = form.save(commit=False)
                todo_item.list = todo_list
                todo_item.save()
            return HttpResponseRedirect('thanks') # Redirect to a 'success' page
    else:
        todo_list_form = TodoListForm()
        todo_item_formset = TodoItemFormSet()

    # For CSRF protection
    # See http://docs.djangoproject.com/en/dev/ref/contrib/csrf/ 
    c = {'todo_list_form': todo_list_form,
         'todo_item_formset': todo_item_formset,
        }
    c.update(csrf(request))

    return render_to_response('todo/index.html', c)

And that’s pretty much it for the server-side stuff!

Client Side Coolness

Now for the template with the JavaScript stuff. The main point here is that whenever a form is added or deleted, the ManagementForm data is also updated, as well as the relevant names and IDs of the form fields.

Here’s the JavaScript (note that I’m using jQuery here):

The JavaScript [View on Github]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
$(document).ready(function () {
    // Code adapted from http://djangosnippets.org/snippets/1389/  
    function updateElementIndex(el, prefix, ndx) {
        var id_regex = new RegExp('(' + prefix + '-\\d+-)');
        var replacement = prefix + '-' + ndx + '-';
        if ($(el).attr("for")) $(el).attr("for", $(el).attr("for").replace(id_regex,
        replacement));
        if (el.id) el.id = el.id.replace(id_regex, replacement);
        if (el.name) el.name = el.name.replace(id_regex, replacement);
    }

    function deleteForm(btn, prefix) {
        var formCount = parseInt($('#id_' + prefix + '-TOTAL_FORMS').val());
        if (formCount > 1) {
            // Delete the item/form
            $(btn).parents('.item').remove();
            var forms = $('.item'); // Get all the forms  
            // Update the total number of forms (1 less than before)
            $('#id_' + prefix + '-TOTAL_FORMS').val(forms.length);
            var i = 0;
            // Go through the forms and set their indices, names and IDs
            for (formCount = forms.length; i < formCount; i++) {
                $(forms.get(i)).children().children().each(function () {
                    if ($(this).attr('type') == 'text') updateElementIndex(this, prefix, i);
                });
            }
        } // End if
        else {
            alert("You have to enter at least one todo item!");
        }
        return false;
    }

    function addForm(btn, prefix) {
        var formCount = parseInt($('#id_' + prefix + '-TOTAL_FORMS').val());
        // You can only submit a maximum of 10 todo items 
        if (formCount < 10) {
            // Clone a form (without event handlers) from the first form
            var row = $(".item:first").clone(false).get(0);
            // Insert it after the last form
            $(row).removeAttr('id').hide().insertAfter(".item:last").slideDown(300);

            // Remove the bits we don't want in the new row/form
            // e.g. error messages
            $(".errorlist", row).remove();
            $(row).children().removeClass("error");

            // Relabel or rename all the relevant bits
            $(row).children().children().each(function () {
                updateElementIndex(this, prefix, formCount);
                $(this).val("");
            });

            // Add an event handler for the delete item/form link 
            $(row).find(".delete").click(function () {
                return deleteForm(this, prefix);
            });
            // Update the total form count
            $("#id_" + prefix + "-TOTAL_FORMS").val(formCount + 1);
        } // End if
        else {
            alert("Sorry, you can only enter a maximum of ten items.");
        }
        return false;
    }
    // Register the click event handlers
    $("#add").click(function () {
        return addForm(this, "form");
    });

    $(".delete").click(function () {
        return deleteForm(this, "form");
    });
});

And the template index.html:

index.html [View on Github]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Dynamic Forms in Django Example</title>

<script type="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/jquery/1.5.0/jquery.min.js">
</script>

</head>
<body>
<h1>Dynamic Forms in Django Example</h1>
<h2>Todo List</h2>
<form action="" method="POST">{% csrf_token %}
    <div class="section">
        {{ todo_list_form.as_p }}
    </div>
    <h2>Todo Items</h2>
    {{ todo_item_formset.management_form }}
    {% for form in todo_item_formset.forms %}
    <div class="item">
      {{ form.as_p }}
      <p style=""><a class="delete" href="#">Delete</a></p>
    </div>
    {% endfor %}
    <p><a id="add" href="#">Add another item</a></p>
    <input type="submit" value=" Submit " />
</form>
</body>
</html>

Credits/References/See Also

-

Comments