WTForms ChosenSelect

8. February 2015. Tagged work, development, python, wtforms.

Recently I often had to build huge selects or even multiple selects and as you might know, especially multi selects can look quite ugly.

Multiple select without styling

Multiple select without styling

But they do not only look ugly, they are also very unnatural to handle, if not for pros at least for casual users, as you have to use shift+alt to select multiple entries, which is not clear to every user. Of course you can add a small description to explain that, but that does not really improve the usability itself.

So what is the alternative? The guys at harvest wrote a very nice javascript plugin that is very much downward compatible and will make your selects and multiple select much more beautiful: chosen. At some point I realized I did not want to manually add a script tag after every field in my templates and decided to write a custom widget to take care of that for me:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import json
from wtforms.widgets import Select, HTMLString


class ChosenSelect(Select):

    def __init__(self, multiple=False, renderer=None, options={}):
        """
            Initiate the widget. This offers you two general options.
            First off it allows you to configure the ChosenSelect to
            allow multiple options and it allows you to pass options
            to the chosen select (this will produce a json object)
            that chosen will get passed as configuration.

                :param multiple: whether this is a multiple select
                    (default to `False`)
                :param renderer: If you do not want to use the default
                    select renderer, you can pass a function that will
                    get the field and options as arguments so that
                    you can customize the rendering.
                :param options: a dictionary of options that will
                    influence the chosen behavior. If no options are
                    given `width: 100%` will be set.
        """
        super(ChosenSelect, self).__init__(multiple=multiple)
        self.renderer = renderer
        options.setdefault('width', '100%')
        self.options = options

    def __call__(self, field, **kwargs):
        """
            Render the actual select.

                :param field: the field to render
                :param **kwargs: options to pass to the rendering
                    (i.e. class, data-* and so on)

            This will render the select as is and attach a chosen
            initiator script for the given id afterwards considering
            the options set up in the beginning.
        """
        kwargs.setdefault('id', field.id)
        # currently chosen does not reflect the readonly attribute
        # we compensate for that by automatically setting disabled,
        # if readonly if given
        # https://github.com/harvesthq/chosen/issues/67
        if kwargs.get("readonly"):
            kwargs['disabled'] = 'disabled'
        html = []
        # render the select
        if self.renderer:
            html.append(self.renderer(self, field, **kwargs))
        else:
            html.append(super(ChosenSelect, self).__call__(field, **kwargs))
        # attach the chosen initiation with options
        html.append(
            '<script>$("#%s").chosen(%s);</script>\n'
            % (kwargs['id'], json.dumps(self.options))
        )
        # return the HTML (as safe markup)
        return HTMLString('\n'.join(html))
1
2
3
4
5
6
from wtforms import Form
from wtforms.fields import Select


class ExampleForm(Form):
    example = Select("Example", choices=[("1", "1"), ("2", "2")], widget=ChosenSelect())

Now to use that you can simply pass widget=ChosenSelect() or if you want a multi select widget=ChosenSelect(multiple=True) to the field setup. As long as you include the chosen.js in your template your select will automatically be converted to a chosen select when your add {{ form.example }} to your template.

And then it might just look like this:

Multiple select with chosen

Multiple select with chosen