Johnston
Johnston

Reputation: 20884

Create selectfield options with custom attributes in WTForms

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

Answers (4)

calabash
calabash

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

phdesign
phdesign

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

Mark
Mark

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

Johnston
Johnston

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

Related Questions