Mimsy Were the Borogoves

Hacks: Articles about programming in Python, Perl, Swift, BASIC, and whatever else I happen to feel like hacking at.

Reusing Django’s filter_horizontal

Jerry Stratton, January 7, 2010

Django’s admin site documentation describes ModelAdmin’s filter_horizontal option as a “nifty unobtrusive JavaScript” to use in place of “the usability-challenged <select multiple>”. HTML’s multiple select does indeed suck for any number of options that require scrolling. Inevitably, when editing an existing entry, you or your users will eventually erase an existing option without knowing it.

Django’s horizontal and vertical filter solutions change these select boxes into one box of unselected options and one box of selected options, making the selected options much more obvious, and making it pretty much impossible to accidentally remove an existing selection.

You can use this JavaScript in your own forms. It consists of several JavaScript files, one CSS file, and a snippet of HTML right next to the pop-up button.

JavaScript and CSS

Assuming that you’ve made use of Django’s popup add form, you already have RelatedObjectLookups.js on your template somewhere. Add several more JavaScript files as well as one CSS file from Django’s built-in library:

  • <script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script>
  • <script type="text/javascript" src="/admin/jsi18n/"></script>
  • <script type="text/javascript" src="/media/js/core.js"></script>
  • <script type="text/javascript" src="/media/js/SelectBox.js"></script>
  • <script type="text/javascript" src="/media/js/SelectFilter2.js"></script>
  • <link rel="stylesheet" type="text/css" href="/media/css/widgets.css" />

Call the JavaScript

If you’re using the admin-form pop-ups as I described earlier in Replicating Django’s admin form pop-ups, you have a template snippet called “form/popupplus.html”. This template is called by both SelectWithPop and MultipleSelectWithPop. Only MultipleSelectWithPop needs filter_horizontal, so add a flag to that class’s render method’s context:

[toggle code]

  • class MultipleSelectWithPop(forms.SelectMultiple):
    • def render(self, name, *args, **kwargs):
      • html = super(MultipleSelectWithPop, self).render(name, *args, **kwargs)
      • popupplus = render_to_string("form/popupplus.html", {'field': name, 'multiple': True})
      • return html+popupplus

And then, inside of popupplus.html, call the SelectFilter JavaScript:

[toggle code]

  • {% if multiple %}
    • <script type="text/javascript">
      • addEvent(window, "load", function(e) {
        • SelectFilter.init("id_{{ field }}", "{{ field }}", 0, "/media/");
      • });
    • </script>
  • {% endif %}

The first parameter to SelectFilter is the field’s ID, the second is the field’s name, the third is 0 for horizontal or 1 for vertical (stacked), and the fourth is the admin media prefix for URLs, usually /media/.

That should be all you need to do to automatically invoke Django’s JavaScript filter for any multiple select form fields that use the pop-up add hack. If you’re not using pop-up adds, you’ll need to override forms.SelectMultiple and create a template for the new Select, just as described in Replicating Django’s admin form pop-ups.

In response to Replicating Django’s admin form pop-ups: Django’s built-in admin form lets you “add another” item in a ForeignKey or ManyToManyField by clicking a plus button. This functionality is very easy to duplicate in your own forms.