Dynamically adding forms to a formset with jQuery

 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 4 years, 1 month ago
  2. Add delete buttons to admin changelist by kylefox 6 years, 11 months ago
  3. Arbitrary length formset by Rupe 4 years, 7 months ago
  4. Complex Formsets by smagala 5 years, 3 months ago
  5. Collapsed stacked inlines by Aneon 4 years, 11 months 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.

#

(Forgotten your password?)