So, it’s two months later than I’d estimated, but sometime this morning, I committed version 1.2. In spite of the delay (which is a story for another time), I’m pretty excited about this release — I got to add a couple of nifty features, and squash some bugs in the process (thanks for the bug reports and patches, guys).
Here’s the list of changes:
- Inline formsets created with “can_delete” set to True are now supported properly; clicking the “remove” link hides the form and sets the DELETE field, so Django handles the deleting when the page is POSTed.
- Added form templates: you can now specify a form that will be cloned to generate the forms in the formset. As a side-effect, you can now delete all forms in a formset, and the “add” link still works as expected.
- Clicking the “add” link now clones the last form, instead of the first; this works much better in the admin, especially if you have one or more extra forms (thanks justhamade).
- Added an optional setting “extraClasses”; set this is an array of CSS classes, and they’ll be applied to the formset’s rows in turn. So, to get the row-striping effect in the Django admin, you’d do something like this:
$('...').formset({extraClasses: ['row1', 'row2']});Adding and removing forms keeps the classes in sync, so your stripes don’t get all mix’d up — hopefully, this is a feature, but time will tell.
- Updated examples and documentation.
As always, you can download the latest release from the Google Code page, or use the links below:
- Source code, minified (using jsmin) version and docs
- Source code, minified version, docs and demo Django project
Advertisement
Great work again.
This has now been added to the django admin in 1.2 and is based on your plugin http://code.djangoproject.com/browser/django/trunk/django/contrib/admin/media/js/inlines.js
http://docs.djangoproject.com/en/dev/ref/contrib/admin/#extra
Thanks for the great work putting this together.
I have found a problem with the management of the delete functionality. The delete state does not persist through validation of the forms.
Say for example you have two forms in the formset. The first one you remove with the intention of deleting it. The second one, you fill out, but it has invalid data. If you then submit this, the page will reload, and the first form will still display.
I have made changes to the jQuery code to resolve this. I am very new to jQuery though, so it might not be the best solution, but it does work.
The diff is below:
87a88,97 > // added to hide > if (del.is(':checked')) { > // this means that the form is already marked for deletion, > // so should hide it to remain consistent (occurs after failed validation) > // at the django end > hiddenDel = row.find('input:hidden[id $= "-DELETE"]'); > hiddenDel.val('on'); > row.hide(); > } > 88a99 >Thanks for the patch Mike…I’ll take a look at it later, though it seems okay. Thanks for the catch!
No problem – least I could do given the work you’ve already done!
FYI, I’ve been working on getting the ordering working with this is as well. It’s taken more code than I expected, and as a jQuery novice might not be the best way of doing it, but will be happy to post what I have when I’m done if you want to take a look?
Hey Mike! Did you ever get the ordering working?
Hey there … I have got ordering together, although it will probably need some work if you wanted to roll it into the project. Usual caveats of it’s working for me but might not for everyone etc ;-)
It also needs some code to deal with the order value in the form processing, but this is exactly the same as it would be for general formset processing …
One other comment I would make is that it can be worth hiding all the forms in a div until after they have been processed by the formset function, so that the checkboxes and order form elements aren’t shown whilst they are being processed.
Hope this is helpful. Here’s the complete diff between 1.2 and my current version:
38,92d37 < // copied the approach directly from the insertDeleteLink method < // to provide a patch for managing moving the formset elements < insertMoveLinks = function(row) { < var moveText = ' <a href="void(0)" rel="nofollow">' + options.upText + '</a> <a href="void(0)" rel="nofollow">' + options.downText + '</a>'; < if (row.is('TR')) { < row.children(':last').append(moveText); < } else if (row.is('UL') || row.is('OL')) { < row.append('' + moveText + ''); < } else { < row.append(moveText); < } < row.find('a.' + options.upCssClass).click(function() { < var row = $(this).parents('.' + options.formCssClass), < order = row.find('input:hidden[id $= "-ORDER"]'), < orderVal = order.val(); < if (orderVal == 1) { < // if we're already at the top, then we can ignore the request < return; < } < else { < // get the previous form element < var prev = row.prev('.' + options.formCssClass); < // move it below the row < // TODO: this has only been tested with one particular implementation, there may be problems < // with different form layout approaches. < row.after(prev); < // change the order values < order.val(parseInt(orderVal)-1); < prev.find('input:hidden[ id $= "-ORDER"]').val(orderVal); < } < }) < row.find('a.' + options.downCssClass).click(function() { < var row = $(this).parents('.' + options.formCssClass), < order = row.find('input:hidden[id $= "-ORDER"]'), < orderVal = order.val(), < formCount = parseInt($('#id_' + options.prefix + '-TOTAL_FORMS').val()); < < if (orderVal == formCount) { < // we're already at the bottom, so we can ignore request < return; < } < else { < // get the next form element < var next = row.next('.' + options.formCssClass); < // move this form element below < next.after(row); < // change the order values < order.val(parseInt(orderVal)+1); < next.find('input:hidden[ id $= "-ORDER"]').val(orderVal); < //updateElementIndex($(row), options.prefix, String(orderVal+1)); < //updateElementIndex($(next), options.prefix, String(orderVal)); < } < }) < < } 143,152d87 < // added to hide < if (del.is(':checked')) { < // this means that the form is already marked for deletion, < // so should hide it to remain consistent (occurs after failed validation) < // at the django end < hiddenDel = row.find('input:hidden[id $= "-DELETE"]'); < hiddenDel.val('on'); < row.hide(); < } < 154,177d88 < < } < // this patch is to be able to work with the order field that is defined by < // the "can_order = True" flag when creating the formset < // Django adds a text field (field name ORDER) with a numeric value < // specifying the order of the form elements. < var order = row.find('input:text[id $= "-ORDER"]'); < if (order.length) { < // check for an order value < // if not defined, check for previous row, get order value from that, increment and assign. < var thisOrderVal = order.val(); < if (!thisOrderVal.length) { < thisOrderVal = row.prev('.' + options.formCssClass).find('input:hidden[id $= "-ORDER"]').val(); < if (thisOrderVal && thisOrderVal.length) { < thisOrderVal = parseInt(thisOrderVal)+1; < } < } < if (!thisOrderVal) { < // if we still don't have a value, we assume that this is the first entry, and it's empty, so < // we can assign it to be 1 < thisOrderVal = 1; < } < order.before(' '); < order.remove(); 181,183d91 < if (order.length) { < insertMoveLinks(row); < } 218,227d125 < // if a form is submitted but there are errors, the form positions in the set < // are reset by django. This code will sort the forms according to their ORDER values < var count = parseInt($('#id_' + options.prefix + '-TOTAL_FORMS').val()); < for (var i=1;i<=count;i++){ < var row = $$.find('input:hidden[id $= "-ORDER"][value="'+String(i)+'"]').parents('.' + options.formCssClass); < if (row.get(0) != $('.' + options.formCssClass + ':last').get(0)) { < $('.' + options.formCssClass + ':last').after(row); < } < } < 249,250d146 < // order patch, need to set the order value for the new entry < row.find('input:hidden[id $= "-ORDER"]').val(formCount+1); 269,272d164 < upText: 'move up', < upCssClass: 'up-link', < downText: 'move down', < downCssClass: 'down-link',Hi, just to let you know, I’ve made some changes to enable support for the empty_form formset attribute that 1.2 now offers. I notice the diff formatting is actually a bit of mess in the comments, let me know if there’s a better way of getting it to you (assuming you want it)
http://docs.djangoproject.com/en/dev/topics/forms/formsets/#empty-form
Thanks for sharing.
The demo won’t work if you have non-ascii characters in the file path of the diretory where the django project lie because of a nasty Python 2.5 bug in os.path.join.
If somebody encounter this problem, just move it to another directory and it works like a charm.
Please not as well that due to jQuery 1.3 limitations, if a user hit the back button, all the fields he added vanhish. It should have been solved in jQuery 1.4.
Cheers
Hey Mike and elo80ka,
Just curious about the integration with the empty_form functionality in 1.2. Having trouble figuring out how to use empty_form to generate a template.
I used trunk, w/ empty_form functionality for my project, which uses inline formsets as rows. Worked awesomely! I had to make a few tweaks locally to the jquery file to make it work with my HTML, but other than that, I really appreciate the good work here.
One thing i noticed about the functionality w/ tabular inlines is that the “add” button disappears when i delete all of my forms from my formsets and then throw a ValidationError in my formset’s clean() method (requires at least 2 forms) and reload the form. This is because there are no more ‘s and $$ is therefore empty so the add button doesn’t get added. I got around this by adding a to my with style=’display:none;’.
This is a great piece of code. I am trying to use it. However, I’m unclear how I can select the widgets of a _particular_ form within a formset (in javascript).
In the form that my formset is bound to I have a javascript function associated with an event on a particular widget. That function ought to change the value of another widget on the form (they are two linked select widgets – the options for each come from a call to a remote server).
I can add and delete forms no problem, thanks to your code.
And I can change the content of one select widget based on the user selected value on the other select widget.
However, when I try to do this on a form that has been added dynamically to the formset, it consistently changes the corresponding select widget on the first form in the formset only. How can I identify particular forms that have been dynamically generated?
Many thanks for your help and time.
@trubliphone: you should be able to re-bind your event handler in the `added` callback (if you downloaded the demo project, take a look at the AutoComplete examples). Basically, you’d have something like this:
$(...).formset({ ... added: function(row) { // Event handlers are cloned with the form, which is why changing // the value in the cloned selects ends up changing the select value // in the first form. // You need to rebind the event handlers to the appropriate form controls. For example: var el = row.find('#firstSelect').unbind(); // Removes all existing handlers el.change(function(){...}); // Rebind the handler, pointing at the right field } })Please consider putting this up on GitHub!
It is on google code already. http://code.google.com/p/django-dynamic-formset/