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

WTForms image picker widget pour Flask avec Bootstrap 4 sans supplément Javascript et CSS

En modifiant les WTforms RadioField ListWidget et en utilisant les boutons Bootstrap 4, nous pouvons construire un joli image picker.

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

Lorsque vous vous inscrivez sur ce site, une image d'avatar vous est attribuée. Bien sûr, vous pouvez changer d'avatar dans "votre compte" et cela se fait à l'aide d'un image picker. De nombreux exemples de image pickers sont disponibles sur Internet. Mais c'est un site Flask incluant WTForms et je veux que le image picker soit généré par la merveilleuse macro Jinja que j'utilise, voir aussi le lien ci-dessous, ok, je l'ai un peu modifié. Avec cette macro, mettre un formulaire sur le modèle est un jeu d'enfant et ressemble à un jeu d'enfant :

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

Le site utilise également Bootstrap 4, donc je préfère ne pas avoir de Javascript et/ou CSS supplémentaires, nous en avons déjà assez ! Il existe de nombreux image picker sur Internet, mais lorsqu'on filtre les résultats avec le WTForms , il ne reste pas grand-chose.

Solution : créer un WTForms widget

Si nous voulons utiliser les ressources disponibles, il n'y a pas d'autre choix que de créer un WTForms image picker widget. Le problème est que la documentation sur ce sujet est limitée et qu'il n'y a pas beaucoup d'exemples. Alors, comment procéder ? Le formulaire image picker est comme un groupe de radio buttons. Sélectionnez une option et soumettez le formulaire. En creusant dans le code WTForms , la classe RadioField est la suivante :

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

Le code ListWidget est utilisé pour produire le code radio buttons HTML . Le 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))

Nous devons 1. faire une copie du ListWidget et 2. le modifier pour qu'il produise également les images de l'avatar. Nous pouvons alors l'utiliser comme suit :

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

Le choix de l'avatar_id est généré dans la fonction d'affichage sous forme de liste de tuples. La valeur sélectionnée est également attribuée par la fonction d'affichage, par exemple :

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

Il n'est pas vraiment difficile de voir comment le ListWidget génère le HTML. La fonction __call__() commence par le code d'ouverture, dans la liste 'html' :

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

Puis, un par un, le code HTML radio button est ajouté à la liste "html" :

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

La balise de fermeture est annexée :

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

Et le HTML est renvoyé en rejoignant la liste 'html' :

    return  HTMLString(''.join(html))

Que contient un subfield ?

Si nous voulons construire notre HTML personnalisé, nous avons besoin de toutes les informations, mais comment les obtenir ? Il doit être dans subfield. Ce que j'ai fait est ce que je fais habituellement, c'est-à-dire imprimer les attributs subfield :

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

Cela a donné les informations suivantes, affichées uniquement pour un des subfield :

     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}

Notez l'attribut coché, il signale l'élément sélectionné. Nous l'utilisons pour construire notre propre HTML.

Utilisation des boutons Bootstrap 4

J'utilise Bootstrap depuis un certain temps maintenant et j'ai pensé que le bouton Bootstrap pourrait être un bon candidat pour le image picker widget. Les images d'avatars utilisées sur le site ont un fond transparent, ce qui est agréable lorsqu'on utilise les boutons Bootstrap . Un bouton sélectionné, survolé, Bootstrap montre un fond et une bordure plus foncés.

L'astuce consiste à mettre le radio button et l'image de l'avatar à l'intérieur du bouton. Je cache également le radio button en utilisant la classe d-none. Enfin, j'entoure les boutons d'une div. Le widget ListImagesWidget ressemble maintenant à

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

Tout cela fonctionne très bien. Il affiche toutes les images et le bouton "Soumettre" situé en dessous. L'image sélectionnée est un peu plus sombre.

Résumé

Ce n'est qu'une des nombreuses façons de mettre en œuvre cette politique. Ici, j'ai copié un code WTForms et je l'ai modifié. Ce qui est bien, c'est que nous n'avons pas à ajouter de Javascript, jQuery et/ou CSS supplémentaires. Si vous voulez voir cela en action, vous devez vous inscrire sur ce site et vous rendre sur votre "compte".

Liens / crédits

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

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.