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.
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
Récent
- Masquer les clés primaires de la base de données UUID de votre application web
- Don't Repeat Yourself (DRY) avec Jinja2
- SQLAlchemy, PostgreSQL, nombre maximal de lignes par user
- Afficher les valeurs des filtres dynamiques SQLAlchemy
- Transfert de données sécurisé grâce au cryptage à Public Key et à pyNaCl
- rqlite : une alternative à haute disponibilité et dist distribuée SQLite
Les plus consultés
- Utilisation des Python's pyOpenSSL pour vérifier les certificats SSL téléchargés d'un hôte
- Utiliser UUIDs au lieu de Integer Autoincrement Primary Keys avec SQLAlchemy et MariaDb
- Utiliser PyInstaller et Cython pour créer un exécutable Python
- Connexion à un service sur un hôte Docker à partir d'un conteneur Docker
- SQLAlchemy : Utilisation de Cascade Deletes pour supprimer des objets connexes
- Flask RESTful API validation des paramètres de la requête avec les schémas Marshmallow