Login

Dynamically adding forms to a formset. OOP version.

Author:
halfnibble
Posted:
May 13, 2015
Language:
JavaScript
Version:
1.7
Score:
1 (after 1 ratings)

What It Is

This is a JavaScript-based solution to dynamically add and remove forms in formsets and inlineformsets. It requires jQuery.

Originally based on this Snippet: https://djangosnippets.org/snippets/1389/

I have done a lot of work to make it OO, and am using it in production on pages with multiple inlineformsets, and even nested inlineformsets (I call it, "Inlineformset Inception").

My hope is that the code and example are enough to show how it works.

Usage Details

In the example usage, I am using a CSS class, 'light', to make every other form have a light background color. My form placeholder is an element with an ID of 'formset-placeholder' (the default). And the form selector is a class name of 'dynamic-form' (the default).

When I have time, I will create a GitHub repository with the code and completed examples.

  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
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
JavaScript
==========

/* For dynamically adding/deleting Django inline formsets
 * Original snippet from: https://djangosnippets.org/snippets/1389/
 * Original snippet by: elo80ka
 * This version by: halfnibble (Josh Wedekind)
 * Updated 5/13/2015
 */

var Formset = (function() {
    var callbacks = {},
        empty_form = {},
        form_count = 0,
        form_manager = {},
        form_placeholder = $('#formset-placeholder')[0],
        form_selector = '.dynamic-form',
        prefix = 'formset';        

    function setup(context) {
        /***************************************
         * Common context attributes           *
         *   prefix:           'formset'       *
         *   form_selector:    '.dynamic-form' *
         *   form_placeholder: DOM.element     *
         *   callbacks:        { funcs... }    *
         ***************************************/
        $.extend(this, context);

        // Set a few more attributes
        this.form_manager = $('#id_' + this.prefix + '-TOTAL_FORMS');
        this.form_count = parseInt(this.form_manager.val());        
        if (this.form_placeholder === undefined || this.form_placeholder === [])
            this.form_placeholder = $('#' + this.prefix + '-placeholder');

        // Setup the empty form for add_form()
        var empty_form = $(this.form_selector + ':first').clone(true).get(0);
        // Clear all values except for hidden field values.
        $(empty_form).find(':input')
            .removeAttr('checked')
            .removeAttr('selected')
            .not(':button, :submit, :reset, [type="hidden"], :radio, :checkbox')
            .val('')
            .attr('value',''); // Handle UpdateView default values
        this.empty_form = empty_form;

        if (this.callbacks.setup)
            this.callbacks.setup(this);
    }

    function add_form() {
        var self = this;

        // Assume new forms get last index counter
        var index = this.form_count;

        // Create form to add
        var form = $(this.empty_form).clone(true).get(0);
        this.update_index(form, index, false);
        
        // Add form after last one, or after the placeholder if none exist.
        if ($(this.form_selector + ':last').length > 0)
            $(form).insertAfter(
                $(this.form_selector + ':last')
            );
        else
            $(form).insertAfter(this.form_placeholder);

        // Update all pertinent form field indexes
        $(form).children('.hidden').removeClass('hidden');
        $(form).find('div, input, select, label, button').each( function() {
            self.update_index(this, index, false);
        });

        // Update index counter
        ++this.form_count;
        this.form_manager.val(this.form_count);

        if (this.callbacks.add_form)
            this.callbacks.add_form(this, form, index);

        return false;
    }

    function delete_form(button) {
        var self = this;
        $(button).parents(this.form_selector).hide(400, function() {
            var form = this;
            $(form).remove();
            --self.form_count;
            self.form_manager.val(self.form_count);

            // Delete anything else before updating
            if (self.callbacks.delete_form)
                self.callbacks.delete_form(self, form);

            // Update all formset indexes, etc.
            self.update_all();
        });

        return false;
    }

    function update_all() {
        var self = this;
        // Get list of all forms in formset
        var forms = $(this.form_selector);

        for (var index = 0; index < this.form_count; index++) {
            /*jshint loopfunc: true */
            var form = forms.get(index);

            // Update form index, then all pertinent field indexes
            this.update_index(form, index);
            $(form).find('div, input, select, label, button')
                .each( function() { self.update_index(this, index); });

            if (this.callbacks.update_all)
                this.callbacks.update_all(this, form, index);
        }
    }

    function update_index(element, index, external_links) { 
        if (external_links === undefined)
            external_links = true; // Default: update links to element ID
        var regex = new RegExp('(' + this.prefix + '-\\d+)');
        var replacement = this.prefix + '-' + index;

        if ($(element).attr("for"))
            $(element).attr(
                "for",
                $(element).attr("for")
                .replace(regex, replacement)
            );
        if (element.id) {
            // Update targets to element
            if (external_links === true)
                $('a[href="#'+element.id+'"]').attr('href', function (i, attr) {
                    return attr.replace(regex, replacement);
                });
            element.id = element.id.replace(regex, replacement);
        }
        if (element.name)
            element.name = element.name.replace(regex, replacement);
        if (element.getAttribute('data-prefix'))
            element.setAttribute(
                'data-prefix',
                element.getAttribute('data-prefix')
                .replace(regex, replacement)
            );

        // Use if you want to replace more field attributes
        // E.g. element.className
        if (this.callbacks.update_index)
            this.callbacks.update_index(this, element, index,
                                        regex, replacement);
    }

    return {
        // Properties
        callbacks: callbacks,
        empty_form: empty_form,
        form_count: form_count,
        form_manager: form_manager,
        form_placeholder: form_placeholder,
        form_selector: form_selector,
        prefix: prefix,
        // Methods
        setup: setup,
        add_form: add_form,
        delete_form: delete_form,
        update_all: update_all,
        update_index: update_index
    };
});



Example Usage
=============

(Placed below formset in template)

<script type="text/javascript">
    <!--
    var callbacks = {
        'setup': function(formset) {
            $(formset.form_selector).find('.delete-form-row').click( function(e) {
                e.preventDefault();
                formset.delete_form(this);
            });
            $('.add-form-row').click( function(e) {
                e.preventDefault();
                formset.add_form();
            });
        }, 

        'add_form': function(formset, form, index) {
            $(form).find('.delete-form-row').click( function(e) {
                e.preventDefault();
                formset.delete_form(this);
            });
            if (index % 2 !== 0)
                $(form).removeClass('light');
        },

        'update_all': function(formset, form, index) {
            if (index % 2 === 0)
                $(form).addClass('light');
            else
                $(form).removeClass('light');
        }
    };

    var formset = new Formset();

    formset.setup({
        prefix: '{{ formset.prefix }}',
        callbacks: callbacks
    });
    //-->
</script>

More like this

  1. Django Collapsed Stacked Inlines by applecat 1 year ago
  2. Django Collapsed Stacked Inlines by mkarajohn 3 years, 2 months ago
  3. Convert multiple select for m2m to multiple checkboxes in django admin form by abidibo 10 years, 11 months ago
  4. Django admin inline ordering - javascript only implementation by ojhilt 11 years, 3 months ago
  5. Google v3 geocoding for Geodjango admin site by samhag 11 years, 4 months ago

Comments

Please login first before commenting.