Login

Dynamically adding forms to a formset with jQuery

Author:
elo80ka
Posted:
March 22, 2009
Language:
JavaScript
Version:
Not specified
Tags:
newforms jquery dynamic-formset
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. Complex Formsets, Redux by smagala 5 years, 3 months ago
  2. Add delete buttons to admin changelist by kylefox 8 years, 1 month ago
  3. Arbitrary length formset by Rupe 5 years, 10 months ago
  4. Complex Formsets by smagala 6 years, 5 months ago
  5. Collapsed stacked inlines by Aneon 6 years, 1 month 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.

#

Please login first before commenting.