Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 529
Johnston Avatar asked May 04 '14 19:05

Johnston


3 Answers

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 ...
like image 77
Mark Avatar answered Nov 09 '22 02:11

Mark


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

like image 36
Johnston Avatar answered Nov 09 '22 04:11

Johnston


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"} })
like image 40
phdesign Avatar answered Nov 09 '22 03:11

phdesign