May
14

WTForms SelectField with Custom Option Attributes

posted on 14 May 2020 in programming

I was surprised to run into this problem in my Flask app, I needed to pass a custom attribute to one of the options in a select list provided by WTForms (in my case I wanted to set the first option as disabled), but it turns out that this is a common problem with lots of work arounds.

Here’s a nice clean solution to pass those custom attributes, keeping the built-in SelectField, but using a custom widget which supports providing attributes for any of the options via a keyed dictionary.

I want to generate markup equivalent to this boostrap 4 example, where the first option is disabled.

<select class="custom-select" id="validationCustom04" required>
    <option selected disabled value="">Choose...</option>
    <option>...</option>
</select>

However there’s no simple way to set a disabled attribute on a specific option. So after reading through the WTForms source code I wrote this 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))

To use it, you’ll need to first pass an instance of CustomSelect as the widget parameter when declaring the field.

customselect = SelectField(
    "Custom Select",
    choices=[("option1", "Option 1"), ("option2", "Option 2")],
    widget=CustomSelect(),
)

Then, when calling the field to render in your template, you can pass a dictionary of option ids (in the format {field_id}-{option_index}) which defines a dictionary of attributes to be passed to the option.

form.customselect(option_attr={"customselect-0": {"disabled": ""} })

Or perhaps you want to pass a data attribute.

form.customselect(option_attr={"customselect-0": {"data-id": "value"} })

Hope this is helpful.