В современной веб-разработке, использование фреймворков, таких как Flask для Python, значительно упрощает создание и обслуживание веб-приложений. Flask предлагает гибкую архитектуру, включая мощные возможности для шаблонизации с помощью движка Jinja2. Рассмотрим разработку пользовательских макросов Jinja для работы с формами. Макросы в Jinja могут сократить повторяющийся код и упростить обслуживание шаблонов, особенно когда речь идет о формах.
Основы работы с Flask и Jinja
Прежде чем перейти к макросам, кратко освежим в памяти, как работать с Flask и Jinja. Flask — это легковесный веб-фреймворк для Python, который позволяет быстро создавать веб-приложения. Jinja — это система шаблонов для Python, используемая по умолчанию в Flask для рендеринга HTML-страниц с динамическим контентом.
Создание мини-проекта
Для наглядности создадим мини-проект веб-приложения на Flask, в котором продемонстрируем использование макроса для формы.
Шаг 1: установка и настройка Flask
Установите Flask через пакетный менеджер:
pip install Flask
Создайте основной файл приложения, например, app.py, и добавьте в него базовый код для запуска Flask-приложения:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def home():
return render_template('form.html')
if __name__ == '__main__':
app.run(debug=True)
Шаг 2: создание HTML-шаблона с формой
В папке templates вашего проекта создайте файл form.html. Это HTML-шаблон формы:
{% from "macros.html" import form_macro %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Форма</title>
</head>
<body>
<form method="post">
{{ form_macro('email', 'Электронная почта') }}
{{ form_macro('password', 'Пароль', type='password') }}
<button type="submit">Отправить</button>
</form>
</body>
</html>
Шаг 3: создание макроса для формы
В том же каталоге templates создайте файл macros.html. В этом файле определите макрос для элементов формы:
{% macro form_macro(name, label, type='text') %}
<div>
<label for="{{ name }}">{{ label }}:</label>
<input type="{{ type }}" id="{{ name }}" name="{{ name }}">
</div>
{% endmacro %}
Файл macros.html содержит только определение макроса. Импорт нужен в том шаблоне, который вызывает макрос, то есть в form.html; в самом macros.html импортировать себя не нужно. Строка импорта для form.html:
{% from "macros.html" import form_macro %}
Дополнительные макросы
рассмотрим создание макросов для более сложных элементов форм, таких как выпадающие списки (select), радиокнопки и чекбоксы. Эти макросы позволят разработчикам еще более эффективно использовать возможности Jinja в Flask, делая код более читаемым и удобным для повторного использования.
Выпадающий список (Select)
Создание выпадающего списка часто требуется для того, чтобы предложить пользователю выбрать один из нескольких вариантов. Вот пример макроса для создания такого списка:
{% macro select_macro(name, label, choices, selected=None) %}
<div>
<label for="{{ name }}">{{ label }}:</label>
<select name="{{ name }}" id="{{ name }}">
{% for value, label in choices %}
<option value="{{ value }}" {% if selected == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
{% endmacro %}
Принимает имя поля, метку, список вариантов выбора и необязательный аргумент, указывающий на уже выбранный вариант. Пример использования:
{% from "macros.html" import select_macro %}
{{ select_macro('fruit', 'Выберите фрукт', [('apple', 'Яблоко'), ('banana', 'Банан'), ('cherry', 'Вишня')]) }}
Радиокнопки
Радиокнопки позволяют выбрать один из нескольких предложенных вариантов. Вот как может выглядеть макрос для радиокнопок:
{% macro radio_macro(name, label, options) %}
<fieldset>
<legend>{{ label }}</legend>
{% for value, description in options %}
<div>
<input type="radio" id="{{ name }}_{{ loop.index }}" name="{{ name }}" value="{{ value }}">
<label for="{{ name }}_{{ loop.index }}">{{ description }}</label>
</div>
{% endfor %}
</fieldset>
{% endmacro %}
Он создает группу радиокнопок с общей меткой. Пример использования:
{% from "macros.html" import radio_macro %}
{{ radio_macro('gender', 'Пол', [('male', 'Мужской'), ('female', 'Женский')]) }}
Чекбоксы
Чекбоксы используются, когда необходимо предоставить возможность выбрать несколько вариантов из предложенных. Вот пример макроса для чекбоксов:
{% macro checkbox_macro(name, label, values, selected=[]) %}
<div>
<label>{{ label }}</label>
{% for value in values %}
<div>
<input type="checkbox" id="{{ name }}_{{ loop.index }}" name="{{ name }}" value="{{ value }}" {% if value in selected %}checked{% endif %}>
<label for="{{ name }}_{{ loop.index }}">{{ value }}</label>
</div>
{% endfor %}
</div>
{% endmacro %}
Он позволяет создать несколько чекбоксов с общей меткой. Пример использования:
{% from "macros.html" import checkbox_macro %}
{{ checkbox_macro('hobbies', 'Хобби', ['Рисование', 'Пение', 'Кодирование'], ['Кодирование']) }}
Добавление этих макросов в вашу библиотеку Jinja значительно облегчит работу с формами, делая ваш код более организованным и удобным для внесения изменений.
Преимущества использования макросов в Jinja
Макросы Jinja предлагают множество преимуществ для разработчиков:
- Повторное использование кода: определяйте элементы формы один раз и используйте их в разных местах вашего приложения.
- Упрощение обслуживания: изменения в структуре или стиле элементов формы нужно вносить только в одном месте.
- Повышение читаемости шаблонов: макросы делают шаблоны более организованными и легкими для понимания.
Radio-макрос Jinja: сохранить checked после отправки формы
Макросы Jinja принимают аргументы и вызываются как функции, а макрос из другого шаблона перед вызовом нужно импортировать (Jinja: Templates). Поэтому расширить radio_macro аргументом selected несложно: остаётся сравнить его со значением варианта через == (Jinja: Templates) и поставить checked только той радиокнопке, которая совпала — в одной группе выбирается лишь одна такая кнопка (MDN: input type=radio).
Почему текущий radio_macro не помнит выбор
Существующий макрос принимает name, label и options, но нигде не выводит checked. Серверный HTML, который возвращается после POST, не восстанавливает выбор: атрибут checked в нём отсутствует у всех вариантов. Поэтому выбранное значение нужно явно передать в макрос из обработчика. При этом отсутствие checked в серверном HTML не означает, что во всех браузерах визуально не будет отмечено ни одной кнопки: по документации MDN Firefox по умолчанию сохраняет динамическое состояние checked между загрузками страницы, и этим поведением можно управлять атрибутом autocomplete.
selected=None и checked по selected == value
Добавьте необязательный аргумент selected=None. В цикле для каждого варианта проверяйте selected == value — оператор == сравнивает два объекта на равенство (Jinja: Templates). Импортируйте макрос в шаблон через {% from "macros.html" import radio_macro %}. В приведённом form.html четвёртый аргумент передаётся при каждом рендере: при первом обычном GET это None, при допустимом POST — выбранная строка.
POST route: request.form.get(channel), allowlist options
По умолчанию маршрут Flask отвечает только на GET, а методы задаются параметром methods (Flask: Quickstart). Чтобы принять POST, укажите methods=["GET", "POST"]. Внутри читайте request.form.get("channel"), сверяйте со списком допустимых значений (allowlist) и возвращайте найденное строковое значение в шаблон как selected_channel. Если значение пустое или не входит в allowlist, передавайте None.
Редакционный пример: готовый артефакт для копирования
Ниже — согласованный редакционный пример из трёх файлов: маршрут Flask, макрос Jinja и шаблон формы. Пример подготовлен редакцией как шаблон для адаптации; он не запускался и не проверялся в живом приложении. Имена поля channel, вариантов options и allowlist согласованы между файлами.
Файл app.py — маршрут с импортами и передачей выбранного значения в шаблон:
# Редакционный пример; не запускался.
from flask import Flask, render_template, request
app = Flask(__name__)
ALLOWED = {"email", "push", "sms"}
OPTIONS = [("email", "Email"), ("push", "Push"), ("sms", "SMS")]
@app.route("/", methods=["GET", "POST"])
def home():
posted = request.form.get("channel")
selected = posted if posted in ALLOWED else None
return render_template(
"form.html",
options=OPTIONS,
selected_channel=selected,
)
if __name__ == "__main__":
app.run(debug=True)
Файл templates/macros.html — макрос radio_macro с аргументом selected=None и условным checked:
# Редакционный пример; не запускался.
{% macro radio_macro(name, label, options, selected=None) %}
<fieldset>
<legend>{{ label }}</legend>
{% for value, description in options %}
<div>
<input type="radio"
id="{{ name }}_{{ loop.index }}"
name="{{ name }}"
value="{{ value }}"
{% if selected == value %}checked{% endif %}>
<label for="{{ name }}_{{ loop.index }}">{{ description }}</label>
</div>
{% endfor %}
</fieldset>
{% endmacro %}
Файл templates/form.html — форма импортирует и вызывает макрос с selected_channel:
# Редакционный пример; не запускался.
{% from "macros.html" import radio_macro %}
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Форма</title>
</head>
<body>
<form method="post">
{{ radio_macro("channel", "Канал", options, selected_channel) }}
<button type="submit">Отправить</button>
</form>
</body>
</html>
Статически ожидаемый результат этого кода: если posted входит в ALLOWED, в шаблон уходит эта строка и checked выводится только у совпавшего варианта; если posted пусто или не входит в allowlist, уходит None, и тогда атрибут checked отсутствует у всех вариантов в серверном HTML. Сверка опирается на allowlist; доверие телу запроса здесь не используется. Пример не запускался. Поэтому речь идёт о выводе checked в сгенерированной разметке. Фактическое состояние выбора в браузере не гарантировано: по документации MDN Firefox по умолчанию сохраняет динамическое состояние checked между загрузками.
Условный пример: три варианта email, push, sms; POST приносит push — тогда selected == value истинно только для варианта push, и в сгенерированном HTML атрибут checked присутствует только у него. Проверка выбора должна опираться на сравнение со allowlist. Доверять телу запроса нельзя: условная таблица ниже показывает результат сверки. Это вывод по коду примера. Проверенное универсальное визуальное состояние браузера здесь не утверждается.
| Случай (условно) | POST channel |
В allowlist? | selected_channel |
Атрибут checked в HTML |
|---|---|---|---|---|
| Совпадение | push |
да | push |
1 |
| Нет совпадения | fax |
нет | None |
0 |
| Пусто | "" |
нет | None |
0 |
Поведение браузера и движка в этих трёх случаях остаётся теоретическим сценарием: редакция его не запускала и не проверяла в живом приложении. Формулировки и код выше — шаблон, который надо адаптировать под собственные имена полей и структуру приложения.
FAQ
Вопрос: Что проверяет тест boolean в Jinja и как он связан с Flask?
Ответ: Тест boolean возвращает истину, если объект является булевым значением (Jinja: Templates). Он добавлен в Jinja 2.11 и проверяет именно тип bool. «Истинность» значения вообще здесь не проверяется. Поэтому условный фрагмент {% if value is boolean %} success! {% endif %} сработает только тогда, когда value — это Python-объект True или False. Строки, пришедшие из формы, булевыми не являются: request.form.get("channel") возвращает строку, и даже строка "True" не пройдёт тест boolean, потому что это str. Тип bool здесь не подходит. Если нужно привести значение из формы к булеву типу, преобразование делайте явно в обработчике и передавайте в шаблон уже настоящий True/False; тогда тест boolean будет вести себя предсказуемо.
Вопрос: Почему после отправки формы ни одна радиокнопка не выбрана?
Ответ: Потому что макрос не принимает ранее выбранное значение и не выводит checked в серверном HTML. Сервер не восстанавливает выбор в возвращённой разметке. При этом отсутствие checked в HTML не гарантирует одинакового визуального состояния во всех браузерах: по документации MDN Firefox по умолчанию сохраняет динамическое состояние checked между загрузками. Чтобы выбор восстанавливался на сервере, значение нужно передать в макрос явно.
Вопрос: Как передать выбранное значение обратно в макрос?
Ответ: Обработчик читает значение поля из отправленных данных, сверяет его со списком допустимых вариантов и передаёт найденную строку в шаблон как значение для сравнения с каждым вариантом.
Вопрос: Что делать, если отправлено значение, которого нет в списке?
Ответ: Сверяйте его со списком допустимых вариантов и при отсутствии совпадения передавайте пустое значение, чтобы ни одна кнопка не была отмечена.
Частые вопросы
Как в Flask вывести данные в label при изменении выбора радиокнопки? Flask и Jinja формируют HTML на сервере до отправки страницы в браузер, поэтому после загрузки страницы они уже не могут обновить label сами по себе. Изменение выбора радиокнопки — это событие в браузере: у <input type="radio"> есть события change и input (MDN: input type=radio). Чтобы label менялся сразу при переключении, нужен небольшой клиентский скрипт, который слушает change на группе радиокнопок и подставляет текст в связанный label. Пример ниже условный и не запускался редакцией; он показывает структуру, где id радиокнопки и for у label совпадают, а отдельный label с id="live-label" служит местом вывода.
<!-- Условный пример; редакция его не запускала. -->
<fieldset>
<legend>Канал</legend>
<div>
<input type="radio" id="channel_email" name="channel" value="email">
<label for="channel_email">Email</label>
</div>
<div>
<input type="radio" id="channel_push" name="channel" value="push">
<label for="channel_push">Push</label>
</div>
</fieldset>
<p>Выбрано: <label id="live-label" for="channel_push">—</label></p>
<script>
const group = document.querySelectorAll('input[name="channel"]');
const out = document.getElementById('live-label');
group.forEach(function (radio) {
radio.addEventListener('change', function () {
out.textContent = radio.value;
});
});
</script>
Если обновление на клиенте не требуется, достаточно показать выбор после отправки формы. Тогда серверный путь остаётся прежним: обработчик читает значение, сверяет его со списком допустимых и передаёт в шаблон, а макрос ставит checked по совпадению. Оба подхода решают разные задачи и могут использоваться вместе.
Почему после отправки формы ни одна радиокнопка не выбрана? Потому что макрос не принимает ранее выбранное значение и не выводит checked в серверном HTML: сервер не восстанавливает выбор в возвращённой разметке. Отсутствие checked в HTML не гарантирует одинакового визуального состояния во всех браузерах — по документации MDN Firefox по умолчанию сохраняет динамическое состояние checked между загрузками. Чтобы выбор восстанавливался на сервере, значение нужно передать в макрос явно.
Как передать выбранное значение обратно в макрос? Обработчик читает значение поля из отправленных данных, сверяет его со списком допустимых вариантов и передаёт найденную строку в шаблон как значение для сравнения с каждым вариантом.
Что делать, если отправлено значение, которого нет в списке? Сверяйте его со списком допустимых вариантов и при отсутствии совпадения передавайте пустое значение, чтобы ни одна кнопка не была отмечена.
Заключение
Использование пользовательских макросов в Jinja для работы с формами в Flask упростит создание веб-приложений и их поддержку. Создавая макросы для повторяющихся элементов формы, вы экономите время на написании кода и делаете его более читаемым и легким для поддержки.
