jQuery plugin: Django Dynamic Formset

Several months ago, I posted this snippet over at Django Snippets. Since then, I’ve used it a few times, and eventually made it into a jQuery plugin. I always intended to release the plugin, but didn’t have time to write some decent documentation, and do a demo project.

After reading the comments on the snippet, I finally decided to bite the bullet this weekend, so here it is. To use it, download one of the releases (I’d recommend the one with the demo project), unzip it, and check out README.txt and INSTALL.txt.

Usage

I’ll assume you’ve already created your formsets. You can create
formsets using any of the provided methods: both regular formsets
(created with the “formset_factory“) and inline formsets
(created with the “inlineformset_factory“) are supported.

I’ll assume you’ve already created your formsets. You can create formsets using any of the provided methods: both regular formsets (created with the “formset_factory“) and inline formsets (created with the “inlineformset_factory“) are supported.

  1. First, copy jquery.formset.js to your MEDIA_ROOT; don’t forget to include the jQuery library too!
  2. Include a reference to the script in your template; again, don’t forget to reference the jQuery library (before the reference to the script).
  3. Render the formset as you normally would — I usually use a table but you can use DIVs, Ps or whatever you desire. Let’s use the example markup below:
    <form id="myForm" method="post" action="">
      <table border="0" cellpadding="0" cellspacing="0">
        <tbody>
        {% for form in formset.forms %}
          <tr>
            <td>{{ form.field1 }}</td>
            <td>{{ form.field2 }}</td>
            <td>{{ form.field3 }}</td>
          </tr>
        {% endfor %}
        </tbody>
      </table>
      {{ formset.management_form }}
    </form>
    
  4. Add the following script to your template (before the closing BODY tag, or in your HEAD, below the reference to jquery.formset.js):
    <script type="text/javascript">
      $(function() {
        $('#myForm tbody tr').formset({
          prefix: '{{ formset.prefix }}'
        });
      })
    </script>
    

    Notice how our jQuery selector targets the container for each form? We could have assigned a class to each TR and used that instead:

      $('.form-container-class').formset(...);
    

    Either way is fine, really :)

  5. That’s it. Fini. Save your template and navigate to the appropriate view in your application, and you should see an “add another” link. Clicking on it should add another instance of your form to the page. You can remove instances by clicking the “remove” link; if there’s only one form in the formset, the remove link isn’t shown.

License

This plugin is released under the New BSD License – use it as you wish.

Quick Download

For the impatient, you can download the releases here:

You might also want to check out the Google Code project.

If you find this useful, feel free to leave a nice comment saying so :) If you find bugs, want to submit a patch, or have an idea for a cool enhancement, submit them to the issue tracker.

Update (3oth Nov, 2009)

Version 1.1 is now available — I’ve updated the download links above to point to it. For details of the changes in 1.1, see the accompanying blog post.

62 thoughts on “jQuery plugin: Django Dynamic Formset

  1. Thanks for this, it’s been a big help on a project I’m working on. One thing I noted: one of my forms has a radio button in it (to control which of the sets of forms is the current instance). When cloning a row, I think it would be better if you explicitly set the checked property of any radio buttons to false.

  2. Hey there, I’ve found your plugin quite useful and it works like charm!

    But I’ve found one caveat, though: when cloning fields with widgets that call javascript, these calls are not updated; p.e: I’m using some DateFields that use the admin Date widget, when creating this fields, Django renders them like this:

    <a href="DateTimeShortcuts.handleCalendarQuickLink(2, 0);" rel="nofollow">Today</a> | <a href="DateTimeShortcuts.openCalendar(2);" rel="nofollow"></a>
    

    Notice the CalendarQuickLink(2, 0) and openCalendar(2) function calls: this is because this field is the third of four date fields (two for each form) my formset has the first time. But when, par example, I add a fifth field with your plugin, the calls of the first one are cloned like this: CalendarQuickLink(0, 0) and openCalendar(0) and thus, the calendar control of this field actually updates another field, i.e., the one that was cloned!

    Checking out the code I kinda realized that it’s almost impossible for your plugin to generically solve this problem, as this javascript calls have very specific names and all, but I have an observation, though: When the fields are first created, I’ve noticed that DIVs for the calendars are created at the end of the document body, for my two-form formset, i.e., four divs are initially created, so the solution could go along the lines of this. I’m really new to this whole javascript thing, so I can’t even begin to imagine a solution to this problem I have, any help you could provide me would be greatly appreciated.

  3. Hi again, I’m glad to tell you that the clarity of your code and the excellent examples you have provided with the plugin (along with the prowess of jQuery) have helped me to solve my problem: I simply passed a function to the parameter ‘added’ of your formset function. It wasn’t easy to find out the solution, though, as I’m new to javascript and jquery (tonight was the first time I actually wrote anything in jQuery) but I managed to solve my problem in four lines of code, so, congratulations! because without your examples and the care to have understandable code you obviously took, I would have been defeated by an, actually, insignificant problem.

  4. @lfborjas: thanks for the extremely detailed feedback – I’m glad you solved your problem. Would you mind posting your solution (or a link to it) here? If it’s okay with you, I could add it to the examples, in case someone else has the same issue. Cheers!

  5. anonymous

    Thank you! Very helpful thing for my projects.

    What about if need more than one formset on a page?
    It’s good, there is a feature to override setup plugin defaults, but one class we can’t override, it’s the dynamic-form. And then you have several formsets, elements of that class conflicts each others. So I added an option in defaults and replace with it the dynamic-form everywhere. Now I have something like this for two fromsets in the javscript section:

               $(function() {
                   $('#formset1Table tbody tr').formset({
                       prefix: '{{ formset1.prefix }}',
                       addCssClass: 'add-formset1-row',
                       deleteCssClass: 'delete-formset1-row',
                       dynamicFormClass: 'dynamic-formset1-form'
                   });
               })
    
               $(function() {
                   $('#formset2Table tbody tr').formset({
                       prefix: '{{ formset2.prefix }}',
                       addCssClass: 'add-formset2-row',
                       deleteCssClass: 'delete-formset2-row',
                       dynamicFormClass: 'dynamic-formset2-form'
                   });
               })
    

    Now it works for me. I don’t know jQuery very well and maybe I’m wrong somewhere.

  6. I’ll gladly post it, I guess only the javascript of it will be enough, so I’ll paste it here along with the xhtml of the problem I was just talking about:
    First, the xhtml was like this:

    <table class="form-container dynamic-form">
      <tbody>
        <tr>
          <th></th>
          <td>
            <input id="id_form-0-display_name" type="text" maxlength="50" name="form-0-display_name"/>
          </td>
        </tr>
        <tr>
          <th>[...]</th>
          <td>
            <input id="id_form-0-start_date" class="vDateField" type="text" size="10" name="form-0-start_date"/>
            <span>
              <a href="javascript:DateTimeShortcuts.handleCalendarQuickLink(0, 0);">Today</a> | <a id="calendarlink0" href="javascript:DateTimeShortcuts.openCalendar(0);"></a>
            </span>
          </td>
        </tr>
       [rest of the form]
    

    As you can see, each form field is contained within a form row, and the form itself, in a table with the form-container class, in the page, after including jquery and your plugin, I also include this script:

               $(function() {
                   $('.form-container').formset({
                	   'added': function(row){
                	   //find the fields with the calendar widget
                	   $(row).find('.vDateField').each(function(i){
                		 //remove the cloned spam element: it links to an incorrect calendar
                		   $(this).parent().find('span').remove();
                		 //DateTimeShortcuts is in the django admin widgets
                		   DateTimeShortcuts.addCalendar(this);           		               		   
                	   });
                	  }
                   });
               })
    

    As you see, I only removed the cloned span(that had, as you can see in the html, calls to the calendar functions with the cloned form id, and not the clonee’s) Then I used the addCalendar function, that resides within the django admin widgets, this one creates divs for the calendars and adds calls to the functions that display them in a new span within this row, this new span will have the correct calls to the handleCalendar and openCalendar functions (which update a textfield and show the calendar, respectively).

    I suppose that I could get rid of the calendar divs left behind in the ‘removed’ parameter of the formset function, but I don’t see why, as it doesn’t affect the functionality at all and only a little on the performance of element queries (though I don’t look-up dom elements at that level of the xhtml).

    Again, thank you for sharing this plugin, and I wish success in your activities.

  7. andreas

    nice plugn!!

    i really needed such an implementation.
    anyway the “add another” link doesnt work on a simple implementation of an inline_formset.
    even if i copy-paste your formset template its the same. remove works, add not

    firebug doesnt report about errors…

    any suggestions?

  8. @andreas: “{{ formset.prefix }}” uses django’s template syntax to insert the actual formset prefix, assuming your formset is named “formset” in the template context. If your formset has a different name (for example, “my_ultra_cool_formset”), you’d write instead:

    $(...).formset({
        prefix: "{{ my_ultra_cool_formset.prefix }}"
    });
    

    I’m guessing in your case, your formset was named something else in the template context, but its prefix just happened to be “formset”? I’ll see if I can make this clearer in the docs :)

  9. Dane

    Hi,

    Great plugin. Thanks. My only suggestion is in the part of the documentation where you explain how to add multiple formsets to a page, you should include that it’s necessary to define a unique ‘addCssClass’ and ‘deleteCssClass’ for each formset, in addition to the prefix and ‘formCssClass’, otherwise there are issues with the add and remove links not behaving properly across formsets.

    -Dane

  10. @Dane: ‘addCssClass’ and ‘deleteCssClass’ shouldn’t have to be unique for each formset. Can you tell me more about the specific issues you encountered? What version of the plugin were you using? Might be a bug; I’ll definitely look into it, thanks.

  11. Lars

    @elo80ka: If deleteCssClass is not unique across formsets, “remove” links show up on formset A when the “add” link is clicked in formset B (even when formset A has only one entry). Clicking this new “remove” link removes the last/only entry in formset A

  12. Paulo Almeida

    Thank you for a great plugin. One problem I’m having is that when using inline formsets an exception is raised when I click ‘remove’ on a form that is filled and then submit the whole form:

    Exception Type:  ValueError
    Exception Value: invalid literal for int() with base 10: ''
    

    Maybe that’s because the processing of inline formsets expects all existing instances to still be there when you submit? If that’s the case, would it be possible to eliminate the ‘remove’ links for the initial instances?

  13. @paulo: I’ve fixed this in trunk…sorta. If you create your inline formset with “can_delete” set to True, the plugin replaces the default “DELETE” checkbox with a hidden field; clicking on the “remove” link hides the form (instead of removing it from the DOM) and sets the value of the hidden field appropriately so Django deletes the instance when the form is posted. This seemed more usable, rather than have both a remove link and a DELETE checkbox.

    Unfortunately, if “can_delete” is False, the default behaviour remains (since there’s no way to tell what kind of formset you’re using, clientside). I haven’t quite worked out a good solution to this yet — I’m leaning towards providing an (optional) setting for the formset type.

    Thanks for the feedback!

  14. Paulo Almeida

    Hi, thanks for the reply. My workaround for this was to comment out the line in the Javascript that renders the ‘remove’ links, as they are not crucial to me.

    My ideal solution (but I don’t know if that’s possible), would be to pass a variable to the Javascript with the number of previously existing instances for each form, and render the ‘remove’ link only for newly added forms. I prefer to have the delete checkbox, because it’s less prone to accidental deletion of data already in the database. As a bonus, this solution would work for both cases of ‘can_delete’.

    Anyway, this is working great for me as it is, with no ‘remove’ links at all.

  15. Paulo Almeida

    Hello again,

    I had another problem: some fields in my form have dynamic help_text strings that are different for each field. When using this module, the “add_another” link would clone the first form in the set, including the help_text, which was inappropriate. My workaround was to find this line:

    var row = $(‘.’ + options.formCssClass + ‘:first’).clone(true).get(0);

    and edit to:

    var row = $(‘.’ + options.formCssClass + ‘:last’).clone(true).get(0);

    It did the trick, and as far as I could tell there shouldn’t be any side effects. Right?…

  16. Pingback: Django: Adding inline formset rows without JavaScript « Falcon Hat Web Development

  17. Bruce

    I’ve picked up your latest trunk version and found your new handling of the DELETE checkbox works very well (and, BTW, for those hand-crafting their forms just using formset_factory, the formset’s DELETE
    checkbox can be manually inserted in a template using {{ form.DELETE }}) … so many thanks for this useful update.

    It appears that “no REMOVE URL when num. of forms == 1” has gone away … is that correct? Or am I doing something else wrong?

    Thanks again for a really useful plugin!

    – Bruce

  18. @Bruce: you’re quite right, it has. I clone the last form, and cache that, so you can safely delete all the forms and things still work as you’d expect. I doubt you’re doing anything wrong :)

    I’d hoped to have updated the documentation (I think the examples are up to date) and packaged another release, but kinda had to take a break, for medical reasons. Hope I’ll be able to get to it this weekend.

    Thanks for the feedback. Cheers!

  19. Bruce

    Thanks for your response!

    Probably worth noting that Django has a bug (reported and discussed quite a bit as
    #10828) which causes formsets.py to die in _get_deleted_forms if one tries to retrieve
    formset.deleted_forms or form.cleaned_data after deleting the last form in the formset. The bug
    is in 1.1.1 and in the 1.2 beta but fixed in the trunk version.

    I picked up the trunk fix for this and merged it into the 1.1.1 version of formsets.py. Your
    plugin is now happily deleting all forms in a formset.

    Thanks again for providing this plugin!

    Cheers –

    Bruce

  20. Benjamin Wohlwend

    Wow, great plugin. This just saved me half a day’s work of hacking up something barely functioning. Thanks a lot!

  21. Nick

    Hey I’m having problems getting this plugin to work.

    I keep getting $(‘#id_events_table tbody tr’).formset is not a function..

    I’ve got jquery 1.3 (I’ve also tried with 1.4). AND I’m loading your jquery.formset.js before the script that tries to load that formset.

    I’m not the biggest JS wiz in the world…but it seems to me like something’s not loading right and I’m having problems getting it figured out. Is there anything you can think of that might be blowing this up??

    Thanks

  22. Nick

    I’ll poke around some more…

    The obvious thing is that somethings not being loaded. But when I remove jquery.js I get an error about jquery not being loaded…so there must be something with jquery.formset.js not being loaded…

    I’ll play with it and thanks for the link hopfully that helps.

  23. Nick

    well…it would appear that they are getting loaded cause on the output of `runserver` I’m getting:

    [03/Oct/2010 12:28:51] “GET /site_media/static/js/jquery-1.3.2.min.js HTTP/1.1” 304 0
    [03/Oct/2010 12:29:22] “GET /site_media/static/js/jquery.formset.min.js HTTP/1.1” 304 0

    while 304 isn’t a 200…it is a positive result…and would lead me to believe that they are being properly loaded, right?

    (sorry for being spammy on your comments)

  24. Pingback: links for 2011-01-30 | toshism

  25. Mark Kecko

    This is working great for us, thanks. However, it mangles the “id” property, while handing the “name” property properly. We have 4 fields defined on the form and we’re clicking “add another” to add the 5th.
    Here’s the resultant field: input id=”credit-5-id_credit-4-name” name=”credit-5-name”.
    Django handles the formset properly (based on name) but we can’t depend on the id working consistently. We’re using the latest version of django-dynamic-formset and jquery 1.4.2. Here’s how we’re calling it:

    $(function() {
      $('#id_credit_table tbody tr').formset({
        extraClasses: ['row1', 'row2', 'row3'],
        prefix: '{{ formset.prefix }}'
      })
    })
    

    Any help would be appreciated.

  26. My apologies Mark — this has been a long-standing issue (which I really should’ve fixed before now), caused by a buggy regex. It’s been fixed in trunk (soon to become v1.3). If you’d rather wait for a release, you can find the fix here.

  27. Oscar

    Hi,

    Very nice script, I’m using it with jQuert 1.6.3 and django 1.3.1 without problem but in my use case, the columns can vary during the process of adding rows (is a table which you can add columns and rows) and I have a problem with the template. It only gets executed at load time, so it does not get the current column number when you click “Add row”.

    How can I do to get the column number when clicking the add row button? I can’t figure it out.

  28. Mbuso

    Thanks for a great script. Worked beautifully for me. I wanted to initially have all formsets hidden and just show the “Add another” link to display the formset. This was as simple as adding a click trigger for the delete link.

  29. Hi, great work, thanks for sharing. Trying to use twitter bootstrap css, i’ve had an issue while replacing deleteCssClass with something like ‘btn btn-warning’. It seems like the fact that css class contains blank space causes the “delete” link not to work properly. I’m quite a newby with jquery, so any tip would be a great help.

  30. @dmat: Thanks! I’m guessing you’re passing ‘deleteCssClass: “btn btn-warning”‘ in the plugin settings? Don’t do that :)
    I believe the recommended way is to use LESS Mixins, provided by Bootstrap. So you’d define something like this in your CSS file:

    a.delete-row {
      .btn
      .btn-warning
    }
    

    Where ‘a.delete-row’ is the default CSS class applied to the delete links.

    Take a look at this article for more information.

  31. Pingback: Django Form ; dynamic form, form clone « djangogal

  32. Derek

    Hi; I am not getting this to work properly. The display is OK, and the javascript works in the webpage (add/delete rows etc.) but the data in the extra rows that are dynamically created is not actually processed (the ones created beforehand work as normal); it is as though Django has no idea they have even been created. The view cannot iterate through them and they are not saved to the database. I am working with Django 1.4.

  33. Mark

    Derek, were you ever able to resolve your issues? I’m having the same trouble, where any dynamically added fields don’t return their values in the request.POST data.

  34. @Mark, can you inspect the DOM using Firebug? Look at the names being generated for the dynamic forms — they should look something like `–`. Also, make sure TOTAL_FORMS is equal to the number of forms in the formset, and that MAX_FORMS is not set.

  35. Mark

    I’m looking through firebug right now. My setup is multiple inline formsets on the same page. They are separated by js tabs, but I don’t expect this is having any impact.

    The original id for one of my text fields is something like id=”id_fs4-0-Text”, and when I add a new row the new id elements are formatted like id=”fs4-1-id_fs4-0-Text” (where fs4 is the prefix for that particular inline formset). This is the only thing that strikes me as being incorrect.

    All of the management_form data is functioning as needed.

    I’m new to web development so I’m not really sure what to look for. My thought as of now is to try to match the original id formatting.

    Thanks for your help.

  36. Mark

    The names for these fields are name=”fs4-0-Text” and name=”fs4-1-Text”, respectively. Also, is there a reason for not setting max_forms, aside from the limit possibly affecting the POST data?

  37. Brilliant! I love your plugin, I spent a short while learning how to implement it, but now that I figured it out, it runs beautifully… kudos!!

    Small question: is there any way to set MAX_FORMS or otherwise limit the number of entries being added? I need at most a constant number.

    Thanks again!

  38. Pingback: Django Dynamic Formset JQuery Library « Code and Chaos

  39. @yaelgrossman: Glad you found it useful. You can pass a value for the `max_forms` parameter in Django, and the plugin will respect this value (trunk only, for now — I REALLY need to package this into a release sometime this week).

  40. Okay, now I’ve expanded my query to a form including two different formsets, and it does not work. I’m encountering the same problem as mentioned above: only the first item of each formset is recognized, and it’s as if the others do not exist: TOTAL_FORMS = 1.

    I am using two formsets, containing forms with a single textual field. They are prefixed fs1 and fs2.

    If I submit, for instance, for formset1: ‘a’, ‘b’, ‘c’ and for formset2: ‘d’, ‘e’, ‘f’, only ‘a’ and ‘d’ will appear in cleaned_data.

    Digging deeper, I can find traces of the other submissions in request.POST. For formset1, the contents of the first form (‘a’) in formset1 appear properly as fs1-0-field1. I can also find traces that appear as form-NaN-fs1-0-field1. Its value is the last text field I entered in formset1 (‘c’). This means that all the forms in the formset are being overwritten by the last entry, EXCEPT the first.

    Is this making any sense at all…? Do you have any idea what might be wrong?

  41. Just what I wanted! Thank you for sharing.
    I would have the need to leave the user at least one inline … something like min_forms = 1. This feature already ‘exists, or should I change the code?
    @yaelgrossman: what line you are getting that error?
    diego

  42. @diegox80: Hi Diego. `min_forms` isn’t supported at the moment, though it shouldn’t be too difficult to add. Feel free to hack the source, and, maybe submit a patch? Cheers!

  43. diegox80

    In order to preserve at least one form, I added:

    isSame = function(a,b) {
    return a.is($(b));
    }
    .
    .
    insertDeleteLink = function(row) {
    // If this is the first form do anything
    if (!isSame($$.first(), row)) {
    .
    .

    and finally:

    insertDeleteLink(template);

    if in formset_factory extra=0,
    otherwise would be used as tamplate the last form that is also the first and without delete link.

    Not great, but
    for me it’s okay ..

    I’m not the best with javascript :p

  44. Vinicius Motta

    Hi, I’ve implemented correctly the plugin and works wonderfully, but I’ve been having a problem.

    I’m trying to implement a javascript to update some things automatically in one of my fields. With the another method of adding and deleting rows without js it worked greatly. But, as it reloaded the page to do so, I’m using this one, but, even though I’ve adapted my code to the new way, it only works on the first row. I was wondering if you’d have any idea of why it’s only working on only 1 row with your add/delete row code.

    The code in js for the first two rows is like that:
    $(‘#id_enclosure-0-year_installed’).change(function(){
    $(‘#id_enclosure-0-chronological_age’).val(year – this.value);
    })
    ;$(‘#id_enclosure-0-service_life’).change(function(){
    var value = document.getElementById(‘id_enclosure-0-effective_age’).value;
    $(‘#id_enclosure-0-remaining_service_life’).val(this.value – value );
    });

    $(‘#id_enclosure-1-year_installed’).change(function(){
    $(‘#id_enclosure-1-chronological_age’).val(year – this.value);
    })
    ;$(‘#id_enclosure-1-service_life’).change(function(){
    var value = document.getElementById(‘id_enclosure-1-effective_age’).value;
    $(‘#id_enclosure-1-remaining_service_life’).val(this.value – value );
    });

    Thanks in advance.

  45. Thank you very much for creating this; it’s super simple to use and obviously very handy. I wanted to ask, though–has the ‘max_num’ support been packaged into a release yet? I’m using 1.2, but setting ‘max_num’ in inlineformset_factory doesn’t seem to be working.

  46. @gchorn: I’ll do a release tonight.
    I added support for `max_num` in trunk, but that was way back (Django 1.2)…I’d like to test against the latest version of Django, and update the sample project and examples.
    Hope you don’t mind waiting :-)

  47. I like the helpful information you provide in your articles.
    I’ll bookmark your blog and check again here frequently. I am quite sure I’ll learn
    plenty of new stuff right here! Good luck for the
    next!

  48. Pingback: Creating editable HTML tables with Django - ErrorsFixing

Leave a comment