Django form field choices, adding an attribute

27,037

Solution 1

You'd have to subclass the field to take whatever means of specifying the title you'd like and the widget to display the new attribute.

If you had something like this (note: entirely untested):

from django import forms
from django.utils.html import escape
from django.utils.encoding import force_unicode

class SelectWithTitles(forms.Select):
    def __init__(self, *args, **kwargs):
        super(SelectWithTitles, self).__init__(*args, **kwargs)
        # Ensure the titles dict exists
        self.titles = {}

    def render_option(self, selected_choices, option_value, option_label):
        title_html = (option_label in self.titles) and \
            u' title="%s" ' % escape(force_unicode(self.titles[option_label])) or ''
        option_value = force_unicode(option_value)
        selected_html = (option_value in selected_choices) and u' selected="selected"' or ''
        return u'<option value="%s"%s%s>%s</option>' % (
            escape(option_value), title_html, selected_html,
            conditional_escape(force_unicode(option_label)))

class ChoiceFieldWithTitles(forms.ChoiceField):
    widget = SelectWithTitles

    def __init__(self, choices=(), *args, **kwargs):
        choice_pairs = [(c[0], c[1]) for c in choices]
        super(ChoiceFieldWithTitles, self).__init__(choices=choice_pairs, *args, **kwargs)
        self.widget.titles = dict([(c[1], c[2]) for c in choices])

...you should be able to do this:

PLANNING_CHOICES_WITH_TITLES = (
    ('0', 'Every morning', 'bla1'),
    ('1', 'Every night',   'bla2'),
    ('2', 'Never',         'bla3'),
)

planning = forms.ChoiceFieldWithTitles(
    required=True, choices=PLANNING_CHOICES_WITH_TITLES)

Solution 2

Here is a general solution to allow attributes in options in a Select widget, SelectMultiple widget and also in Select2 widgets.

It has been tested on Django 2.1 and should work for other versions as well (tell me in comments).

from django.forms.widgets import Select, SelectMultiple

class SelectWOA(Select):
    """
    Select With Option Attributes:
        subclass of Django's Select widget that allows attributes in options, 
        like disabled="disabled", title="help text", class="some classes",
              style="background: color;"...

    Pass a dict instead of a string for its label:
        choices = [ ('value_1', 'label_1'),
                    ...
                    ('value_k', {'label': 'label_k', 'foo': 'bar', ...}),
                    ... ]
    The option k will be rendered as:
        <option value="value_k" foo="bar" ...>label_k</option>
    """

    def create_option(self, name, value, label, selected, index, 
                      subindex=None, attrs=None):
        if isinstance(label, dict):
            opt_attrs = label.copy()
            label = opt_attrs.pop('label')
        else: 
            opt_attrs = {}
        option_dict = super(SelectWOA, self).create_option(name, value, 
            label, selected, index, subindex=subindex, attrs=attrs)
        for key,val in opt_attrs.items():
            option_dict['attrs'][key] = val
        return option_dict

Here is an example that you can try in your forms.py:

choices = [('b', 'blue'),
           ('g', {'label': 'green', 'disabled': 'disabled'}),
           ('c', {'label': 'cyan', 
                  'title': 'Kind of violet',
                  'style': 'background: cyan;',
                 }),
           ('r', 'red'), ]

colors = forms.ChoiceField(
    label="Colors",
    choices=choices,
    widget=SelectWOA)

The colors field can be rendered in Django shell to check the result:

(myvenv) $ ./manage.py shell
>>> from myapp.forms import *
>>> choices = ...
>>> colors = ...
>>> colors.widget.render('mycolors','')
'''<select name="mycolors">
      <option value="b">blue</option>
      <option value="g" disabled="disabled">green</option>
      <option value="c" title="Kind of violet" style="background: cyan;">cyan</option>
      <option value="r">red</option>
 </select>'''

To allow multiple selections, add this:

class SelectMultipleWOA(SelectWOA, SelectMultiple):
    """ 
    SelectMultipleWOA widget works like SelectMultiple, with options attrs.
    See SelectWOA.
    """
    pass

colors = forms.MultipleChoiceField(
    label="Colors",
    choices=choices,
    widget=SelectMultipleWOA)

It will render <select name="mycolors" multiple>...<\select>.

You can use SelectWOA and SelectMultipleWOA to extend the Select2 widgets:

from django_select2.forms import Select2Mixin

class Select2MultipleWidgetWOA(Select2Mixin, SelectMultipleWOA):
    """
    Select2 drop in widget for multiple select.
    Works just like Select2MultipleWidget but with options attrs.
    """
    pass

colors = forms.MultipleChoiceField(
    label="Colors",
    choices=choices,
    widget=Select2MultipleWidgetWOA(
        attrs={'data-placeholder': 'Any color',
               'data-close-on-select': 'false',
               'style': 'width:280px; height:36px;',
               'title': 'Type a word to filter the menu',}
    )
)

It will render something like:

'''<select name="mycolors" data-placeholder="Any color" 
    class="django-select2" data-minimum-input-length="0" multiple 
    style="width:280px; height:36px;" data-close-on-select="false" 
    data-allow-clear="false" title="Type a word to filter the menu">
       <option value="b">blue</option>
       <option value="g" disabled="disabled">green</option>
       <option value="c" title="Kind of violet" style="background: cyan;">cyan</option>
       <option value="r">red</option>
    </select>'''

Solution 3

This is how I solved the problem

#models.py
class Publisher(models.Model):
    slug = models.Slugfield()

class Book(forms.ModelForm):
    publisher = models.ForeignKey(Publisher)

#forms.py
from django import forms
from django.utils.encoding import force_unicode
from django.utils.html import escape, conditional_escape

class SelectWithData(forms.Select):
    def render_option(self, selected_choices, option_value, option_label):
        obj_data = {
            obj.id: {
                data_attr: getattr(obj, data_attr) for data_attr in self.data_attrs
            } for obj in self.queryset
        }

        data_text = u''
        for data_attr in self.data_attrs:
            data_text += u' data-{}="{}" '.format(
                data_attr, 
                escape(force_unicode(obj_data[option_value][data_attr]))
            )

        option_value = force_unicode(option_value)
        selected_html = (option_value in selected_choices) and u' selected="selected"' or ''
        return u'<option value="{}"{}{}>{}</option>'.format(
            escape(option_value),
            data_text,
            selected_html,
            conditional_escape(force_unicode(option_label))
        )


class ModelChoiceFieldWithData(forms.ModelChoiceField):
    widget = SelectWithData

    def __init__(self, queryset, **kwargs):
        data_attrs = kwargs.pop('data_attrs')
        super(ModelChoiceFieldWithData, self).__init__(queryset, **kwargs)
        self.widget.queryset = queryset
        self.widget.data_attrs = data_attrs


class BookForm(forms.ModelForm):

    publisher = ModelChoiceFieldWithData(
        queryset=Publisher.objects.all(),
        data_attrs=('slug',),
    )

#html

<select  id="id_publisher" name="publisher" required="required" title="">
    <option value="1" data-slug="first" selected="selected">First Publisher</option>
    <option value="2" data-slug="second">Second Publisher</option>
</select>

Solution 4

render_option has been removed from Django 1.11 onwards. This is what I did to achieve this. A little bit of digging and this seems straightforward. Works with Django 2.0+

class CustomSelect(forms.Select):
    def __init__(self, attrs=None, choices=()):
        self.custom_attrs = {}
        super().__init__(attrs, choices)

    def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
        index = str(index) if subindex is None else "%s_%s" % (index, subindex)
        if attrs is None:
            attrs = {}
        option_attrs = self.build_attrs(self.attrs, attrs) if self.option_inherits_attrs else {}
        if selected:
            option_attrs.update(self.checked_attribute)
        if 'id' in option_attrs:
            option_attrs['id'] = self.id_for_label(option_attrs['id'], index)

        # setting the attributes here for the option
        if len(self.custom_attrs) > 0:
            if value in self.custom_attrs:
                custom_attr = self.custom_attrs[value]
                for k, v in custom_attr.items():
                    option_attrs.update({k: v})

        return {
            'name': name,
            'value': value,
            'label': label,
            'selected': selected,
            'index': index,
            'attrs': option_attrs,
            'type': self.input_type,
            'template_name': self.option_template_name,
        }


class MyModelChoiceField(ModelChoiceField):

    # custom method to label the option field
    def label_from_instance(self, obj):
        # since the object is accessible here you can set the extra attributes
        if hasattr(obj, 'type'):
            self.widget.custom_attrs.update({obj.pk: {'type': obj.type}})
        return obj.get_display_name()

The form:

class BookingForm(forms.ModelForm):

    customer = MyModelChoiceField(required=True,
                                  queryset=Customer.objects.filter(is_active=True).order_by('name'),
                                  widget=CustomSelect(attrs={'class': 'chosen-select'}))

The output which I needed is as:

  <select name="customer" class="chosen-select" required="" id="id_customer">
      <option value="" selected="">---------</option>
      <option value="242" type="CNT">AEC Transcolutions Private Limited</option>
      <option value="243" type="CNT">BBC FREIGHT CARRIER</option>
      <option value="244" type="CNT">Blue Dart Express Limited</option>

Solution 5

You can't. Not without too much hackery, at least.

You are probably good using form_utils.BetterForm, an external package rather than django.forms.Form if you want to have this and more control over the markup generated.

Share:
27,037

Related videos on Youtube

Hellnar
Author by

Hellnar

Updated on July 09, 2022

Comments

  • Hellnar
    Hellnar over 1 year
    PLANNING_CHOICES = (
        ('0',u'Every morning'),
        ('1',u'Every night'),
        ('2',u'Never'),
    )
    
    planning = forms.ChoiceField(required=True, choices = PLANNING_CHOICES)
    

    Having a such form field named planning, I need to add title attribute to choices and in the end renders as this:

    <select>
        <option value="0" title="bla1">Every morning</option>
        <option value="1" title="bla2">Every night</option>
        <option value="2" title="bla3">Never</option>
    </select>
    

    How can it be achieved ?

  • BigglesZX
    BigglesZX over 4 years
    Fantastically useful, thank you! All I wanted was to be able to render some extra html attributes with my <options>. This was the most understandable solution and on Django 1.11 it worked great.
  • Maddie Graham
    Maddie Graham over 4 years
    I think that this is the only valid and important answer to the question asked. Previous responses do not work with new versions of django or relate to other forms option (like forms.ModelForm), not ChoiceField as in the basic question. Thank you 'Edouard Thiel'.
  • A K
    A K almost 4 years
    Good work! I approved a little his solution, see here: stackoverflow.com/a/59375625/2881558
  • Saleh
    Saleh over 1 year
    This is the best solution I found on stack overflow for this type of question "add attributes to options by customizing select widget"

Related