angle-up arrow-clockwise arrow-counterclockwise arrow-down-up arrow-left at calendar card-list chat check envelope folder house info-circle pencil people person person-plus phone plus question-circle search tag trash x

WTForms image picker widget for Flask with Bootstrap 4 without extra Javascript and CSS

24 January 2020 Updated 24 January 2020 by Peter

By modifying the WTforms RadioField ListWidget and using Bootstrap 4 buttons we can build a nice image picker.

post main image
https://unsplash.com/@heftiba

When you sign up for this website you are assigned an avatar image. Of course you can change the avatar in 'your account' and this is done using an image picker. Many image pickers examples can be found on the internet. But this is a Flask site including WTForms and I want the image picker to be generated by the wonderful Jinja macro I am using, see also link below, ok, I modified it a bit. With this macro putting a form on the template is a breeze and looks like:

{% from "macros/wtf_bootstrap.html" import bootstrap_form %}
{% extends "content_full_width.html" %}
{% block content %}

	{{ bootstrap_form(form) }}

   	{% if back_button %}
		<a href="{{ back_button.url }}" class="btn btn-outline-dark btn-lg mt-4" role="button">
			{{ back_button.name }}
		</a>
	{% endif %}

{% endblock %}

The site also uses Bootstrap 4 so I prefer to have no extra Javascript and/or CSS, we got enough already! There are many image pickers on the internet but when filtering the results with WTForms not much remain.

Solution: create a WTForms widget

If we want to use the available resources there is no other choice then to create a WTForms image picker widget. Problem is that the documentation on this is limited and there are not much examples. So how to proceed? The image picker form is like a group of radio buttons. Select one and submit the form. When digging into the WTForms code, the RadioField class is as follows:

lib64/python3.6/site-packages/wtforms/fields/core.py
class RadioField(SelectField):
    """
    Like a SelectField, except displays a list of radio buttons.

    Iterating the field will produce subfields (each containing a label as
    well) in order to allow custom rendering of the individual radio fields.
    """
    widget = widgets.ListWidget(prefix_label=False)
    option_widget = widgets.RadioInput()

The ListWidget is used to output the radio buttons HTML code. The WTForms code:

lib64/python3.6/site-packages/wtforms/widgets/core.py
class ListWidget(object):
    """
    Renders a list of fields as a `ul` or `ol` list.

    This is used for fields which encapsulate many inner fields as subfields.
    The widget will try to iterate the field to get access to the subfields and
    call them to render them.

    If `prefix_label` is set, the subfield's label is printed before the field,
    otherwise afterwards. The latter is useful for iterating radios or
    checkboxes.
    """
    def __init__(self, html_tag='ul', prefix_label=True):
        assert html_tag in ('ol', 'ul')
        self.html_tag = html_tag
        self.prefix_label = prefix_label

    def __call__(self, field, **kwargs):
        kwargs.setdefault('id', field.id)
        html = ['<%s %s>' % (self.html_tag, html_params(**kwargs))]
        for subfield in field:
            if self.prefix_label:
                html.append('<li>%s %s</li>' % (subfield.label, subfield()))
            else:
                html.append('<li>%s %s</li>' % (subfield(), subfield.label))
        html.append('</%s>' % self.html_tag)
        return HTMLString(''.join(html))

What we must do is 1. make a copy of the ListWidget and 2. modify it so that it also will output the avatar images. Then we can use this as follows:

class ListImagesWidget(object):
   ...
   our modified ListWidget code 
   ...

class SelectImageField(SelectField):

    widget = ListImagesWidget(prefix_label=False)
    option_widget = RadioInput()


class AccountAvatarEditForm(FlaskForm):

    avatar_id = SelectImageField(_('Select your avatar'))

    submit = SubmitField(_l('Update'))

The avatar_id choices is generated in the view function as list of tuples. The selected value is also assigned by the view function, for example:

    form.avatar_id.choices = [('default.png', 'default.png'), ('avatar1.png', 'avatar1.png'), ...]
    form.avatar_id.data = 'default.png'

It is not really difficult to see how the ListWidget generates the HTML. The __call__() function starts with the opening code, in the 'html' list:

    html = ['<%s %s>' % (self.html_tag, html_params(**kwargs))]

Then one by one the HTML radio button code is appended to the 'html' list:

        html.append('<li>%s %s</li>' % (subfield(), subfield.label))

The closing tag is appended:

    html.append('</%s>' % self.html_tag)

And the HTML is returned by joining the 'html' list:

    return HTMLString(''.join(html))

What is in a subfield?

If we want to build our custom HTML we need all information but how can we get this? It must be in subfield. What I did was what I usually do and that is print the subfield attributes:

    for subfield in field:
        current_app.logger.debug(fname + ': subfield.__dict__ = {}'.format(subfield.__dict__))

This gave the following information, shown only for one of the subfields:

    subfield.__dict__ = {
        'meta': <wtforms.form.Meta object at 0x7ff388eb6750>, 
        'default': None, 
        'description': '', 
        'render_kw': None, 
        'filters': (), 
        'flags': <wtforms.fields.Flags: {}>, 
        'name': 'avatar_id', 
        'short_name': 'avatar_id', 
        'type': '_Option', 
        'validators': [], 
        'id': 'avatar_id-0', 
        'label': Label('avatar_id-0', 'default.png'), 
        'widget': <wtforms.widgets.core.RadioInput object at 0x7ff38989fb10>, 
        'process_errors': [], 
        'object_data': 'default.png', 
        'data': 'default.png', 
        'checked': True}

Note the checked attribute, it signals the selected item. We use this to build our own HTML.

Using Bootstrap 4 buttons

I have been using Bootstrap for some time now and thought the Bootstrap button might be a good candidate for the image picker widget. The avatar images used on the site have a transparent background which is nice when using Bootstrap buttons. A selected, hovered, Bootstrap button shows a darker background and border.

The trick is to put the radio button and the avatar image inside the button. I also hide the radio button using the d-none class. Finally I enclose the buttons by a div. The ListImagesWidget now looks like:

class ListImagesWidget(object):

    def __init__(self, html_tag='ul', prefix_label=True):
        assert html_tag in ('ol', 'ul')
        self.html_tag = html_tag
        self.prefix_label = prefix_label

    def __call__(self, field, **kwargs):
        fname = 'ListImagesWidget - __call__'

        kwargs.setdefault('id', field.id)

        html = ['<div data-toggle="buttons">']

        for subfield in field:
            if self.prefix_label:
                # never used, see caller: prefix_label=False
            else:

                checked = ''
                active = ''
                if subfield.checked:
                    checked = 'checked="checked"'
                    active = 'active'

                avatar_img = '<img src="' + avatars_images_url() + '/' + str(subfield.label.text) + '" class="img-fluid rounded-circle w-75" alt="">'

                button_html = '''
                    <button class="btn btn-light border-secondary mt-2 mr-2 mb-2 {active}">
                        <input type="radio" class="d-none" name="{subfield_name}" id="{subfield_id}" autocomplete="off" value="{subfield_data}" {checked}>
                        {avatar_img}
                    </button>
                    '''.format( active=active,
                                subfield_name=subfield.name,
                                subfield_id=subfield.id,
                                subfield_data=subfield.data,
                                checked=checked,
                                avatar_img=avatar_img,
                )
                html.append(button_html)

        html.append('</div>')
        return HTMLString(''.join(html))

This all works very nice. It shows all the images and submit button below them. The image selected is a little bit darker. 

Summary

This is just one of many ways to implement this. Here I copied some WTForms code and modified it. The nice thing is that we do not have to add extra Javascript, jQuery and/or CSS. If you want to see this in action you must register with this site and go to your 'Account'.

Links / credits

bootstrap-wtf
https://github.com/carlnewton/bootstrap-wtf

Buttons - Bootstrap
https://getbootstrap.com/docs/4.4/components/buttons/

WTForms - widgets
https://wtforms.readthedocs.io/en/stable/widgets.html

Leave a comment

Comment anonymously or log in to comment.

Comments

Leave a reply

Reply anonymously or log in to reply.