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

WTForms image picker widget para Flask con Bootstrap 4 sin Javascript extra y CSS

Modificando los WTforms RadioField ListWidget y usando los botones Bootstrap 4 podemos construir un bonito image picker.

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

Cuando te registras en este sitio web se te asigna una imagen de avatar. Por supuesto que puedes cambiar el avatar en 'tu cuenta' y esto se hace usando un image picker. Muchos ejemplos de image pickers se pueden encontrar en Internet. Pero este es un sitio Flask incluyendo WTForms y quiero que el image picker sea generado por la maravillosa macro Jinja que estoy usando, ver también el enlace de abajo, ok, lo modifiqué un poco. Con este macro poner un formulario en la plantilla es pan comido y parece:

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

El sitio también usa Bootstrap 4 por lo que prefiero no tener Javascript y/o CSS extra, ya tenemos suficiente! Hay muchos image pickers en Internet, pero cuando se filtran los resultados con WTForms no queda mucho.

Solución: crear un WTForms widget

Si queremos utilizar los recursos disponibles no hay otra opción que crear un WTForms image picker widget. El problema es que la documentación sobre esto es limitada y no hay muchos ejemplos. Entonces, ¿cómo proceder? El formulario image picker es como un grupo de radio buttons. Seleccione uno y envíe el formulario. Cuando se profundiza en el código WTForms , la clase RadioField es la siguiente:

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

El ListWidget se utiliza para emitir el código radio buttons HTML . El código 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))

Lo que debemos hacer es 1. hacer una copia del ListWidget y 2. modificarlo para que también emita las imágenes de los avatares. Entonces podemos usar esto de la siguiente manera:

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

Las opciones de avatar_id se generan en la función de vista como lista de tuples. El valor seleccionado también es asignado por la función de vista, por ejemplo:

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

No es realmente difícil ver cómo el ListWidget genera el HTML. La función __call__() comienza con el código de apertura, en la lista 'html':

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

Luego, uno por uno, el código HTML radio button se agrega a la lista 'html':

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

Se adjunta la etiqueta de cierre:

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

Y el HTML es devuelto al unirse a la lista 'html':

    return  HTMLString(''.join(html))

¿Qué hay en un subfield?

Si queremos construir nuestro HTML personalizado necesitamos toda la información, pero ¿cómo podemos obtenerla? Debe estar en subfield. Lo que hice fue lo que suelo hacer y es imprimir los atributos subfield :

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

Esto dio la siguiente información, que se muestra sólo para uno de los 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}

Observe el atributo marcado, que señala el elemento seleccionado. Usamos esto para construir nuestro propio HTML.

Utilizando los botones Bootstrap 4

He estado usando Bootstrap desde hace algún tiempo y pensé que el botón Bootstrap podría ser un buen candidato para el image picker widget. Las imágenes de avatar utilizadas en el sitio tienen un fondo transparente, lo cual es agradable cuando se utilizan los botones Bootstrap . Un botón seleccionado, que se encuentra en el aire, Bootstrap muestra un fondo y un borde más oscuros.

El truco está en poner el radio button y la imagen del avatar dentro del botón. También oculto el radio button usando la clase d-none. Finalmente encierro los botones por un div. El ListImagesWidget ahora se ve como:

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

Todo esto funciona muy bien. Muestra todas las imágenes y el botón de envío debajo de ellas. La imagen seleccionada es un poco más oscura.

Resumen

Esta es sólo una de las muchas maneras de implementar esto. Aquí copié algo de código WTForms y lo modifiqué. Lo bueno es que no tenemos que añadir Javascript, jQuery y/o CSS adicionales. Si quieres ver esto en acción debes registrarte en este sitio e ir a tu 'Cuenta'.

Enlaces / créditos

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

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.