Django form field choices, adding an attribute
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.
Related videos on Youtube
Hellnar
Updated on July 09, 2022Comments
-
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 over 4 yearsFantastically 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 over 4 yearsI 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 almost 4 yearsGood work! I approved a little his solution, see here: stackoverflow.com/a/59375625/2881558
-
Saleh over 1 yearThis is the best solution I found on stack overflow for this type of question "add attributes to options by customizing select widget"