Create selectfield options with custom attributes in WTForms
Asked Answered
H

4

13

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.

Hedges answered 4/5, 2014 at 19:50 Comment(5)
Checkout this post - #23024288 I think you about summed it up.Malarkey
@Malarkey Thank you for confirming this. I greatly appreciate it.Hedges
Being that you know how it works now and several people want to do it. You could even consider submitting a patch to the WTForms project.Malarkey
@Malarkey You can see the whole convo here: github.com/wtforms/wtforms/pull/81Hedges
interesting thats awesome thanks for the research!Malarkey
A
8

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 ...
Auriga answered 10/10, 2017 at 4:1 Comment(7)
This is the solution I was looking for. Thanks mate.Nazareth
@jibinmathew Does this validate? I get too many values to unpack (expected 2), in SelectField.pre_validate() (when it's enumerating choices). This is with WTForms-2.1 (which is old...)Urdu
Good point - I missed the pre_validate override. I've updated the answer.Auriga
@NealGokli Have you got the solution?Nazareth
@jibinmathew I don't, but try Mark's edit above! I decided I didn't need it for what I was working on, but I may have to use it for something else soon anyway!Urdu
Thanks, you saved my world. Surprising that this is not in the default behavior.Quizmaster
For anything using this in 2022 - HTMLString is no longer used in project, you can replace it with Markup (from flask import Markup)Lamentation
H
5

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

Hedges answered 6/5, 2014 at 18:57 Comment(1)
How do you actually modify the form to integrate these extra option ?Pedestrianism
E
3

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"} })
Entwine answered 12/5, 2020 at 21:58 Comment(1)
This is great for my usecase (even when using QuerySelectFiled), thank you! Just slighty simplified: def __call__(self, field, option_attr={}, **kwargs)Lamentation
B
0

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>
Bertie answered 17/1, 2022 at 20:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.