Reputation: 20884
I am trying to create a SelectField
or SelectMultipleField
that allows me to add attributes to it's <option>
tags. I am trying to add attributes like data-id
or another data-____
. I have not been able to figure out how to do this as it only seems possible to add attributes to the <select>
tag itself and not the options.
The end result should be something like:
<select id="regularstuff-here" name="regular-name-here">
<option value="1" data-id="somedata here" >Some Name here</option>
<option value="2" data-id="somedata here" >Some Name here</option>
</select>
I assume I have to create a custom widget. If I look at the source for WTForms I see that select
widget calls:
html.append(self.render_option(val, label, selected))
If I look at that method:
@classmethod
def render_option(cls, value, label, selected, **kwargs):
options = dict(kwargs, value=value)
if selected:
options['selected'] = True
return HTMLString('<option %s>%s</option>' % (html_params(**options),
escape(text_type(label))))
So it does not seem that you can pass any extra params to the method that renders the option
tags.
Upvotes: 13
Views: 6892
Reputation: 453
I'm not sure if I'm reading the requirement correctly but I had this same requirement - that is to add to choices in a SelectField. In my case I just wanted to add an option that said, 'Select an option...' as SelectField doesn't have an option for a blank entry like QuerySelectField does. Which is needed for using javascript onchange trigger. But you could add data.id, data.value or whatever.
I just did this in the flask route like so:
# populate choices for Category drop down
categories = Classification.query.filter_by(selectable=True).all()
all_cats = [cat.service for cat in categories]
unique_cat = list(dict.fromkeys(all_cats)) # remove duplicate names for Category drop down
unique_cat.sort() #sort alphabetically
unique_cat.insert(0, 'Choose a category...') # add this as first option in the drop down so onchange js is triggered
form.category.choices = unique_cat
The last two lines being the most relevant for our requirement. If I look at the generated HTML it now has the extra element:
<select class="form-control" id="category" name="category">
<option value="Choose a category...">Choose a category...</option>
<option value="Accounts">Accounts</option>
<option value="Business Applications">Business Applications</option>
</select>
Upvotes: 0
Reputation: 2157
As an alternative to Mark's answer, here's a custom widget (which is the field 'renderer') that allows passing option attributes at render time.
from markupsafe import Markup
from wtforms.widgets.core import html_params
class CustomSelect:
"""
Renders a select field allowing custom attributes for options.
Expects the field to be an iterable object of Option fields.
The render function accepts a dictionary of option ids ("{field_id}-{option_index}")
which contain a dictionary of attributes to be passed to the option.
Example:
form.customselect(option_attr={"customselect-0": {"disabled": ""} })
"""
def __init__(self, multiple=False):
self.multiple = multiple
def __call__(self, field, option_attr=None, **kwargs):
if option_attr is None:
option_attr = {}
kwargs.setdefault("id", field.id)
if self.multiple:
kwargs["multiple"] = True
if "required" not in kwargs and "required" in getattr(field, "flags", []):
kwargs["required"] = True
html = ["<select %s>" % html_params(name=field.name, **kwargs)]
for option in field:
attr = option_attr.get(option.id, {})
html.append(option(**attr))
html.append("</select>")
return Markup("".join(html))
When declaring the field, pass an instance of CustomSelect
as the widget
parameter.
customselect = SelectField(
"Custom Select",
choices=[("option1", "Option 1"), ("option2", "Option 2")],
widget=CustomSelect(),
)
When calling the field to render, pass a dictionary of option ids ("{field_id}-{option_index}") which define a dictionary of attributes to be passed to the option.
form.customselect(option_attr={"customselect-0": {"data-id": "value"} })
Upvotes: 3
Reputation: 3557
If you (like me) want to store the custom attributes on the choices array, per choice, rather than supplying at render time, the following customised "AttribSelectField" and widget should help. The choices become a 3-tuple of (value, label, render_args) instead of a 2-tuple of (value, label).
from wtforms.fields import SelectField
from wtforms.widgets import Select, html_params, HTMLString
class AttribSelect(Select):
"""
Renders a select field that supports options including additional html params.
The field must provide an `iter_choices()` method which the widget will
call on rendering; this method must yield tuples of
`(value, label, selected, html_attribs)`.
"""
def __call__(self, field, **kwargs):
kwargs.setdefault('id', field.id)
if self.multiple:
kwargs['multiple'] = True
html = ['<select %s>' % html_params(name=field.name, **kwargs)]
for val, label, selected, html_attribs in field.iter_choices():
html.append(self.render_option(val, label, selected, **html_attribs))
html.append('</select>')
return HTMLString(''.join(html))
class AttribSelectField(SelectField):
widget = AttribSelect()
def iter_choices(self):
for value, label, render_args in self.choices:
yield (value, label, self.coerce(value) == self.data, render_args)
def pre_validate(self, form):
if self.choices:
for v, _, _ in self.choices:
if self.data == v:
break
else:
raise ValueError(self.gettext('Is Not a valid choice'))
An Example of usage:
choices = [('', 'select a name', dict(disabled='disabled'))]
choices.append(('alex', 'Alex', dict()))
select_field = AttribSelectField('name', choices=choices, default='')
which outputs the following for the first option
tag:
<option disabled="disabled" selected ...
Upvotes: 8
Reputation: 20884
I just wanted to say that this is possible without monkey patching or rewriting wtforms. The library code does support it although not very straightforwardly. I found this out because I attempted to write a fix for WTForms and submitted a PR myself and found out afterwards that you can just do this (I've spent days trying to figure this out):
>>> from wtforms import SelectField, Form
>>> class F(Form):
... a = SelectField(choices=[('a', 'Apple'), ('b', 'Banana')])
...
>>> i = 44
>>> form = F()
>>> for subchoice in form.a:
... print subchoice(**{'data-id': i})
... i += 1
...
<option data-id="44" value="a">Apple</option>
<option data-id="45" value="b">Banana</option>
See the convo here:
https://github.com/wtforms/wtforms/pull/81
Upvotes: 5