angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

WTForms image picker widget voor Flask met Bootstrap 4 zonder extra Javascript en CSS

Door de WTforms RadioField ListWidget aan te passen en Bootstrap 4 knoppen te gebruiken kunnen we een mooie image picker bouwen.

24 januari 2020
post main image
https://unsplash.com/@heftiba

Wanneer u zich aanmeldt voor deze website krijgt u een avatarafbeelding toegewezen. Natuurlijk kun je de avatar in 'je account' veranderen en dit gebeurt met een image picker. Veel image pickers voorbeelden zijn te vinden op het internet. Maar dit is een Flask site inclusief WTForms en ik wil dat de image picker wordt gegenereerd door de prachtige Jinja macro die ik gebruik, zie ook onderstaande link, ok, ik heb het een beetje aangepast. Met deze macro is het plaatsen van een formulier op het sjabloon een makkie en ziet het er uit als:

{% 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 %}

De site gebruikt ook Bootstrap 4 dus ik heb liever geen extra Javascript en/of CSS, we hebben al genoeg! Er zijn veel image pickers op het internet, maar bij het filteren van de resultaten met WTForms blijft er niet veel over.

Oplossing: maak een WTForms widget aan.

Als we de beschikbare middelen willen gebruiken is er geen andere keuze dan een WTForms image picker widget aan te maken. Het probleem is dat de documentatie hierover beperkt is en dat er niet veel voorbeelden zijn. Dus hoe gaan we nu verder? De image picker vorm is als een groep van radio buttons. Selecteer er één en verzend het formulier. Bij het invoeren van de code WTForms is de klasse RadioField als volgt:

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()

De ListWidget wordt gebruikt om de radio buttons HTML code uit te voeren. De code WTForms :

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))

Wat we moeten doen is 1. een kopie maken van de ListWidget en 2. deze zo aanpassen dat deze ook de avatarafbeeldingen zal uitvoeren. Dan kunnen we dit als volgt gebruiken:

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'))

De avatar_id-keuzes worden in de weergavefunctie gegenereerd als lijst van tuples. De geselecteerde waarde wordt ook toegekend door bijvoorbeeld de weergavefunctie:

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

Het is niet echt moeilijk om te zien hoe de ListWidget de HTML genereert. De __call__() functie begint met de openingscode, in de 'html' lijst:

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

Vervolgens wordt één voor één de code HTML radio button toegevoegd aan de 'html'-lijst:

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

Het sluitzegel is bijgevoegd:

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

En de HTML wordt geretourneerd door zich aan te sluiten bij de 'html' lijst:

    return  HTMLString(''.join(html))

Wat zit er in een subveld?

Als we onze aangepaste HTML willen bouwen hebben we alle informatie nodig, maar hoe kunnen we die krijgen? Het moet in het subveld zijn. Wat ik deed was wat ik gewoonlijk doe en dat is het afdrukken van de subveld attributen:

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

Dit gaf de volgende informatie, die slechts voor één van de subvelden werd getoond:

    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}

Let op het aangevinkte attribuut, het signaleert het geselecteerde item. We gebruiken dit om onze eigen HTML te bouwen.

Met behulp van Bootstrap 4 knoppen

Ik gebruik al enige tijd Bootstrap en dacht dat de Bootstrap knop een goede kandidaat zou kunnen zijn voor de image picker widget. De avatarafbeeldingen die op de site worden gebruikt hebben een transparante achtergrond, wat leuk is bij gebruik van de Bootstrap -knoppen. Een geselecteerde, zwevende, Bootstrap knop toont een donkerdere achtergrond en rand.

De truc is om de radio button en de avatar afbeelding in de knop te plaatsen. Ik verberg ook de radio button met behulp van de d-none klasse. Tenslotte sluit ik de knoppen in met een div. De ListImagesWidget ziet er nu uit:

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))

Dit werkt allemaal heel mooi. Het toont alle foto's en legt de knop eronder vast. De geselecteerde afbeelding is iets donkerder.

Samenvatting

Dit is slechts een van de vele manieren om dit te implementeren. Hier heb ik wat WTForms code gekopieerd en aangepast. Het mooie is dat we geen extra Javascript, jQuery en/of CSS hoeven toe te voegen. Als u dit in actie wilt zien, moet u zich bij deze site registreren en naar uw 'Account' gaan.

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

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.