Login

Dynamically adding forms to a formset with jQuery

Author:
elo80ka
Posted:
March 22, 2009
Language:
JavaScript
Version:
Not specified
Score:
18 (after 18 ratings)

I recently worked on an application, where I had to provide a way for users to search for objects based on user-defined properties attached to these objects. I decided to model the search form using a formset, and I thought it'd be a good idea to allow users dynamically add and remove search criteria.

The script (dynamic-formset.js) should be re-usable as-is:

  1. Include it in your template (don't forget to include jquery.js first!).
  2. Apply the 'dynamic-form' class to the container for each form instance (in this example, the 'tr').
  3. Handle the 'click' event for your add and delete buttons. Call the addForm and deleteForm functions respectively, passing each function a reference to the button raising the event, and the formset prefix.

That's about it. In your view, you can instantiate the formset, and access your forms as usual.

 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
75
76
77
78
79
80
Javascript (dynamic-formset.js):

	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 addForm(btn, prefix) {
        var formCount = parseInt($('#id_' + prefix + '-TOTAL_FORMS').val());
        var row = $('.dynamic-form:first').clone(true).get(0);
        $(row).removeAttr('id').insertAfter($('.dynamic-form:last')).children('.hidden').removeClass('hidden');
        $(row).children().not(':last').children().each(function() {
    	    updateElementIndex(this, prefix, formCount);
    	    $(this).val('');
        });
        $(row).find('.delete-row').click(function() {
    	    deleteForm(this, prefix);
        });
        $('#id_' + prefix + '-TOTAL_FORMS').val(formCount + 1);
        return false;
    }

    function deleteForm(btn, prefix) {
        $(btn).parents('.dynamic-form').remove();
        var forms = $('.dynamic-form');
        $('#id_' + prefix + '-TOTAL_FORMS').val(forms.length);
        for (var i=0, formCount=forms.length; i<formCount; i++) {
    	    $(forms.get(i)).children().not(':last').children().each(function() {
    	        updateElementIndex(this, prefix, i);
    	    });
        }
        return false;
    }

Template (form.html):

    <script type="text/javascript">
    <!--
    $(function () {
        $('.add-row').click(function() {
    	    return addForm(this, 'form');
        });
        $('.delete-row').click(function() {
    	    return deleteForm(this, 'form');
        })
    })
    //-->
    </script>
    <table id="id_forms_table" border="0" cellpadding="0" cellspacing="5">
        <thead>
    	    <tr>
    	        <th scope="col">&nbsp;</th>
    	        <th scope="col">Property name</th>
    	        <th scope="col">&nbsp;</th>
    	        <th scope="col">&nbsp;</th>
    	    </tr>
        </thead>
        <tbody>
            {% for form in property_formset.forms %}
    	    <tr id="{{ form.prefix }}-row" class="dynamic-form">
    	        <td{% if forloop.first %} class="hidden"{% endif %}>{{ form.operand }}</td>
    	        <td>{{ form.property }}</td>
    	        <td>contains {{ form.value }}</td>
    	        <td{% if forloop.first %} class="hidden"{% endif %}>
    	            <a id="remove-{{ form.prefix }}-row" href="javascript:void(0)" class="delete-row"></a>
    	        </td>
            </tr>
    	    {% endfor %}
            <tr>
    	        <td colspan="4"><a href="javascript:void(0)" class="add-row">add property</a></td>
    	    </tr>
        </tbody>
    </table>
    {{ property_form.management_form }}
    <div>
        <input type="submit" value=" Find &raquo; " />
    </div>

More like this

  1. Django Collapsed Stacked Inlines by applecat 1 year, 10 months ago
  2. Django Collapsed Stacked Inlines by mkarajohn 3 years, 11 months ago
  3. Dynamically adding forms to a formset. OOP version. by halfnibble 9 years, 7 months ago
  4. Convert multiple select for m2m to multiple checkboxes in django admin form by abidibo 11 years, 8 months ago
  5. Django admin inline ordering - javascript only implementation by ojhilt 12 years ago

Comments

nubela (on March 28, 2009):

there is an error with updateElementIndex, since it assumes that every child element contains .name and .id, and also it doesn't fix labels.

heres a corrected one:

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); }

#

elo80ka (on June 4, 2009):

Thanks for catching that nubela - I've updated the snippet.

#

martync (on June 23, 2009):

nice snippet many thanks. I'll post an update for make this working with inlineformset soon

#

christian.oudard (on September 23, 2009):

This is a great piece of code! Very useful.

I ran into a problem while using it: It assumes too much about the structure of each row. I, for example, am using fieldsets. I had to change line 15 like so:

$(row).find('input, select, label').each(function() {
...

Does this alter too many elements? Does this leave out any important elements?

#

stormlifter (on October 1, 2009):

Wish we could see your formset in this example. Also did you ever get inlineformset to work?

#

stormlifter (on October 1, 2009):

Also is it just me or does the "delete" function call not have any text and thus is unclickable?

I'm still looking at how it is designed because it seems as though you'd want the delete button to only show if there is more than one row up, but the code doesn't seem to be accomplishing that.

Maybe I'm misunderstanding the code, it's possible.

#

elo80ka (on October 10, 2009):

@stormlifter: to use this with inline formsets, you need to change the prefix in the script on your template. Something like this:

$(function () {
    $('.add-row').click(function() {
        return addForm(this, '{{ formset.prefix }}');
    });
    $('.delete-row').click(function() {
        return deleteForm(this, '{{ formset.prefix }}');
    })
})

Assuming your formset is named "formset" in the template context.

#

AgustinLado (on July 21, 2015):

This snippet evolved into a jQuery plug-in by the same author which you can find here: https://github.com/elo80ka/django-dynamic-formset

#

daroxs95 (on April 13, 2020):

it works like a charm, only one doubt after 5 years, mi delete button renders the id for added forms like:

<button id="remove-form-0-row" class="w3-btn w3-gray delete-row" type="button">Delete form</button>

being the form id: form-(number diferent than 0)-row and i am not shure what problem could bring me this, although so far so good

#

Please login first before commenting.