Add and remove Django-Admin Inlines with JavaScript
The built-in Django Admin-Interface (django.contrib.admin) has a nifty little feature called Inlines or InlineModelAdmins. With Inlines you can edit multiple related objects right on the bottom of the page of the parent object. Looking at the official Django tutorial a Poll-object can have multiple Choice-objects which can then be edited inline with the Poll-object. I'm not going deeper into this now and assume that you are familiar with Inlines.
Current State
If you are using a lot of Inlines on one page it can quickly get messy because
a setting of extra=3
means that with for example 3 different Inlines you get
at least 9 empty Inline forms when loading the page. And if you want to add 5
objects to one of them you first have to add three, click save, wait till the
page refreshes and start adding the last 2 and save again.
Adding a bit JavaScript
Trying to solve this I have written a bit of JavaScript. It's not a complete solution but it works for me (TM) so I'm going to share it here hoping that someone else may find it useful of might improve it further.
First let't handle removing/deleting of already saved Inlines: Every Inline that's present when you load the page has a tiny checkbox on the top-right corner labeled "delete", which will - if checked - delete the Inline once you click save. My naive approach to make this more usable is to just collapse the Inline once it's marked for deletion. The code thas has to be added to the page is just a few lines but uses jQuery:
$(function(){ $('.inline-group .inline-related .delete input').each(function(i,e){ $(e).bind('change', function(e){ if(this.checked) { // marked for deletion $(this).parents('.inline-related').children('fieldset.module').addClass('collapsed collapse') }else{ $(this).parents('.inline-related').children('fieldset.module').removeClass('collapsed') } }) }) })
The code reuses CSS classes already present in the Admin CSS for collapsing fieldsets.
Now let's dynamically add more Inline objects with JavaScript: Adding new Inline forms involves a bit more steps, first we need a button which let's the user add a new form. Looking into the django/contrib/admin/templates/admin/edit_inline/stacked.html template you can see that this button is already included but commented out:
{# <ul class="tools"> #} {# <li><a class="add" href="">Add another {{ inline_admin_formset.opts.verbose_name|title }}</a></li> #} {# </ul> #}
This leaves us with two good solutions: Either add a template to out InlineModelAdmin which overwrites the default inline template and has this Link included or write some JavaScript code to dynamically add this Button at runtime. For simplicity I used the template approach.
Additionally I've added a target to the link. The modified lines in my Inline template (which is essentially just a copy of the default stacked-inline template above) look like this:
<ul class="tools"> <li><a class="add" href="#" onclick="return add_inline_form('{{ inline_admin_formset.formset.management_form.prefix }}')"> Add another {{ inline_admin_formset.opts.verbose_name|title }}</a></li> </ul>
Clicking this link will call a (yet to be defined) JavaScript function called
add_inline_form
which takes the prefix of the Inline formset as a parameter,
this is needed to calculate some values.
The following JS code has to be added to the page:
function increment_form_ids(el, to, name) { var from = to-1 $(':input', $(el)).each(function(i,e){ var old_name = $(e).attr('name') var old_id = $(e).attr('id') $(e).attr('name', old_name.replace(from, to)) $(e).attr('id', old_id.replace(from, to)) $(e).val('') }) } function add_inline_form(name) { var first = $('#id_'+name+'-0-id').parents('.inline-related') var last = $(first).parent().children('.last-related') var copy = $(last).clone(true) var count = $(first).parent().children('.inline-related').length $(last).removeClass('last-related'); $(last).after(copy); $('input#id_'+name+'-TOTAL_FORMS').val(count+1) increment_form_ids($(first).parents('.inline-group').children('.last-related'), count, name); return false; }
This code takes care of adding a new Inline form (by cloning the last one and resetting values) and incrementing id and name attributes on the new form and incrementing the counter of how many inline forms for the Inline exist (which is stored in a hidden form-field).
Conslusion
I am able to add Inline forms on the fly and collapse Inlines which are marked for deletion, which both are huge usability improvements for the enduser using the page to edit data. The code above is at most a prototype to show that this is possible and may break at any point. I've only tested it with pretty simple Inline models (containing CharFields). If you have ideas for improvements feel free to take the code and do whatever you want with it.
Sidenote: Also check out Simon's sort snippet to sort Inlines dynamically using drag-and-drop.
Thank you very much, very usefull and well explained
Geschrieben von pach 2 Monate, 2 Wochen nach Veröffentlichung des Blog-Eintrags am 30. Jan. 2009, 13:07. Antworten
Nice solution, thanks :)
Geschrieben von Denis 3 Monate, 1 Woche nach Veröffentlichung des Blog-Eintrags am 19. Feb. 2009, 16:41. Antworten
nice, really nice!
Geschrieben von affendaFlek 5 Monate, 1 Woche nach Veröffentlichung des Blog-Eintrags am 17. April 2009, 16:41. Antworten
Thank you so much for your post ;)
Geschrieben von Valerio 6 Monate, 2 Wochen nach Veröffentlichung des Blog-Eintrags am 24. Mai 2009, 19:17. Antworten
I've been looking for something like this in a django-way, to avoid the use of javascript, but given the circumstances this seems the cleanest way to do it :-)
Geschrieben von Mauro 7 Monate nach Veröffentlichung des Blog-Eintrags am 12. Juni 2009, 04:20. Antworten
Thanks! BTW, I added support for tabular inlines to the add_inline_form() function and made it increment the number in the header for stacked inlines. Here's the code: http://www.djangosnippets.org/snippets/1594/
Geschrieben von MasonM 7 Monate, 2 Wochen nach Veröffentlichung des Blog-Eintrags am 25. Juni 2009, 02:29. Antworten
Really nice. What do you advise in case of having multiples inline forms?? Something else I noted is that you only increment the id and name for inputs not for selects. Thanks
Geschrieben von David Cifuentes 7 Monate, 3 Wochen nach Veröffentlichung des Blog-Eintrags am 1. Juli 2009, 20:32. Antworten
Multiple Inline-Forms should work fine. I'm using this code on a project where one page has 4 different inline object-types.
AFAIK Selects are not handled at the moment because I don't needed them at the time of writing the code. This would be a great improvement though.
Geschrieben von Arne 7 Monate, 3 Wochen nach Veröffentlichung des Blog-Eintrags am 2. Juli 2009, 10:22. Antworten
i worked a solution like this on 0.96
i used to show delete for not saved forms too, to avoid validation on required fields ;)
Geschrieben von Carlos De Smedt 8 Monate, 3 Wochen nach Veröffentlichung des Blog-Eintrags am 4. Aug. 2009, 06:54. Antworten
Does it work under Django 1.1? I'm trying to do something similar, but it doesn't work :/
When I change TOTAL_FORMS using JavaScript, form doesn't validates - it throws global form error message, but there is no field error. It works only if total_forms is equal to initial_forms + extra. Any help?
Geschrieben von Jaro 1 Jahr nach Veröffentlichung des Blog-Eintrags am 17. Nov. 2009, 17:15. Antworten
Great post!
Under Django 1.1.1 the code was adding and removing entire tables instead of just the table rows (I don't know if something changed between the time the article was written and now to cause this).
I've updated the code so that it works with the admin interface from Django 1.1.1 and only operates on rows. You can find it here:
http://evan.borgstrom.ca/post/543916687/a-better-add-and-remove-inline-objects-from-the
Geschrieben von Evan Borgstrom 1 Jahr, 5 Monate nach Veröffentlichung des Blog-Eintrags am 23. April 2010, 23:35. Antworten