How do I use Django's MultiWidget?

John Mee picture John Mee · Aug 18, 2010 · Viewed 17.9k times · Source

The documentation is a bit lacking with respect to this feature.

from django import forms

class TwoInputWidget(forms.MultiWidget):
    """An example widget which concatenates two text inputs with a space"""
    def __init__(self, attrs=None):
        widgets = [forms.TextInput, forms.TextInput]

I can see I need to create a "widgets" property with a list of other widgets, but after that it gets a little Sherlock Holmes.

Would someone please explain to me how to use the MultiWidget widget?

Answer

user257111 picture user257111 · Jan 12, 2011

Interesting question and I think perhaps deserving of a little more attention in the docs.

Here's an example from a question I've just asked:

class DateSelectorWidget(widgets.MultiWidget):
    def __init__(self, attrs=None, dt=None, mode=0):  
        if dt is not None:
            self.datepos = dt
        else:
            self.datepos = date.today()    

        # bits of python to create days, months, years
        # example below, the rest snipped for neatness.

        years = [(year, year) for year in year_digits]

        _widgets = (
            widgets.Select(attrs=attrs, choices=days), 
            widgets.Select(attrs=attrs, choices=months),
            widgets.Select(attrs=attrs, choices=years),
            )
        super(DateSelectorWidget, self).__init__(_widgets, attrs)

    def decompress(self, value):
        if value:
            return [value.day, value.month, value.year]
        return [None, None, None]

    def format_output(self, rendered_widgets):
        return u''.join(rendered_widgets)

What've I done?

  • subclassed django.forms.widgets.MultiWidget
  • Implemented a constructor that creates several widgets.WidgetName widgets in a tuple. This is important because the super class uses the existence of this tuple to take care of several things for you.
  • My format output is pass-through, but the idea is that you can add custom html in here should you wish
  • I've also implemented decompress because you have to - you should expect to be passed values from the database in a single value object. decompress breaks this up for display in the widget. How and what you do here is up to you and depends on the widget.

Things I haven't, but could have, overriden:

  • render, this is actually responsible for rendering widgets, so you definitely need to call the super render method if you subclass this. You can change how things are displayed just before rendering by subclassing this.

Example, django markitup's render method:

def render(self, name, value, attrs=None):
    html = super(MarkItUpWidget, self).render(name, value, attrs)

    if self.auto_preview:
        auto_preview = "$('a[title=\"Preview\"]').trigger('mouseup');"
    else: auto_preview = ''

    html += ('<script type="text/javascript">'
            '(function($) { '
             '$(document).ready(function() {'
             '  $("#%(id)s").markItUp(mySettings);'
             '  %(auto_preview)s '
             '});'
             '})(jQuery);'
             '</script>' % {'id': attrs['id'],
                            'auto_preview': auto_preview })
    return mark_safe(html)
  • value_from_datadict - See my question here. value_from_datadict pulls the value associated with this widget out of the data dictionary of all submitted data with this form. In the case of a multiwidget representing a single field, you need to reconstruct that value from your multiple sub-widgets, which is how the data will have been submitted.
  • _get_media might be useful for you if you want to retrieve media using django's representation of media. The default implementation cycles the widgets asking for the media; if you subclass it and are using any fancy widgets you need to call the super; if your widget needs any media then you need to add it using this.

For example, markitup's django widget does this:

def _media(self):
        return forms.Media(
            css= {'screen': (posixpath.join(self.miu_skin, 'style.css'),
                             posixpath.join(self.miu_set, 'style.css'))},
            js=(settings.JQUERY_URL,
                absolute_url('markitup/jquery.markitup.js'),
                posixpath.join(self.miu_set, 'set.js')))
    media = property(_media)

Again, it is creating a tuple of paths to the correct location, just as my widget has created a tuple of widgets in the __init__ method.

I think that covers it for important parts of the MultiWidget class. What you are trying to do does depend on what you've created/which widgets you're using, which is why I can't go into details easily. However, if you want to see the base class for yourself and take a look at the comments, take a look at the source.