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

WTForms image picker widget для Flask с Bootstrap 4 без лишних Javascript и CSS

Модифицируя формы WTforms RadioField ListWidget и используя кнопки Bootstrap 4, мы можем построить красивый image picker.

24 января 2020
post main image
https://unsplash.com/@heftiba

Когда вы регистрируетесь на этом сайте, вам присваивается изображение аватара. Конечно, вы можете изменить аватар в 'вашем аккаунте', и это делается с помощью image picker. Многие примеры image pickers можно найти в интернете. Но это сайт Flask , включая WTForms , и я хочу, чтобы image picker был сгенерирован замечательным макросом Jinja , который я использую, смотрите также ссылку ниже, ладно, я немного изменил его. С помощью этого макроса нанесение формы на шаблон выглядит как ветерок:

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

Сайт также использует Bootstrap 4, поэтому я предпочитаю, чтобы у нас не было лишних Javascript и/или CSS, мы уже получили достаточно! В интернете много image picker, но при фильтрации результатов WTForms остается не так уж много.

Решение: создать WTForms widget

Если мы хотим использовать доступные ресурсы, то нет другого выбора, тогда создайте WTForms image picker widget. Проблема в том, что документация по этому вопросу ограничена и примеров мало. Ну и как дальше? Форма image picker подобна группе radio buttons. Выберите один и отправьте форму. При копании в код WTForms класс RadioField выглядит следующим образом:

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

ListWidget используется для вывода кода radio buttons HTML . Код 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))

Что мы должны сделать, так это 1. сделать копию ListWidget и 2. изменить ее так, чтобы она также выводила изображения аватары. Тогда мы можем использовать это следующим образом:

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

Выбор avatar_id генерируется в функции просмотра как список tuples. Выбранное значение также назначается, например, функцией просмотра:

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

На самом деле нетрудно увидеть, как ListWidget генерирует HTML. Функция __call__() начинается с кода открытия в списке 'html':

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

Затем код HTML radio button по очереди добавляется в список 'html':

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

Прилагается закрывающий тег:

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

А HTML возвращается путем присоединения к списку 'html':

    return  HTMLString(''.join(html))

Что такое subfield?

Если мы хотим собрать наш пользовательский HTML , нам нужна вся информация, но как мы можем ее получить? Он должен быть в subfield. Я сделал то, что обычно делаю, а именно распечатываю атрибуты subfield :

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

Это дало следующую информацию, показанную только для одного из 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}

Обратите внимание на отмеченный атрибут, он сигнализирует о выбранном элементе. Мы используем это для создания нашего собственного HTML.

Использование кнопок Bootstrap 4

Я использовал Bootstrap в течение некоторого времени и подумал, что кнопка Bootstrap может быть хорошим кандидатом на image picker widget. Изображения аватара, используемые на сайте, имеют прозрачный фон, который приятен при использовании кнопок Bootstrap . Выбранная, зависшая кнопка Bootstrap показывает более темный фон и рамку.

Фокус в том, чтобы поместить radio button и изображение аватара внутрь кнопки. Я также скрываю radio button , используя класс d-none. Наконец-то я прикладываю пуговицы к ныряльщику. Виджет ListImagesWidget теперь выглядит так:

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

Все это работает очень хорошо. Он показывает все изображения и кнопку "Отправить" под ними. Выбранное изображение немного темнее.

Резюме

Это всего лишь один из многих способов реализации этого. Здесь я скопировал немного кода WTForms и модифицировал его. Приятно то, что нам не нужно добавлять дополнительные Javascript, jQuery и/или CSS. Если вы хотите увидеть это в действии, вы должны зарегистрироваться на этом сайте и перейти в вашу "Учетную запись".

Ссылки / кредиты

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

Оставить комментарий

Комментируйте анонимно или войдите в систему, чтобы прокомментировать.

Комментарии

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.