Отрисовка форм
Внешний вид форм может быть очень разным. На практике мы можем
столкнуться с двумя крайностями. С одной стороны, существует
необходимость отрисовывать в приложении множество форм, визуально
похожих друг на друга как две капли воды, и мы оценим простую отрисовку
без шаблона с помощью $form->render()
. Обычно это относится к
административным интерфейсам.
С другой стороны, существуют разнообразные формы, где действует правило: каждая — уникальна. Их вид лучше всего описать языком HTML в шаблоне формы. И, конечно, помимо этих двух крайностей, мы столкнемся с множеством форм, находящихся где-то посередине.
Отрисовка с помощью Latte
Система шаблонов Latte существенно упрощает отрисовку форм и их элементов. Сначала мы покажем, как отрисовывать формы вручную по отдельным элементам и тем самым получить полный контроль над кодом. Позже мы покажем, как можно такую отрисовку автоматизировать.
Проект Latte-шаблона для формы можно сгенерировать с
помощью метода Nette\Forms\Blueprint::latte($form)
, который выведет его на
страницу браузера. Затем достаточно кликнуть, чтобы выделить код, и
скопировать его в проект.
{control}
Самый простой способ отрисовать форму — написать в шаблоне:
{control signInForm}
Повлиять на вид отрисованной таким образом формы можно с помощью конфигурации Renderer и отдельных элементов.
n:name
Определение формы в PHP-коде можно очень легко связать с HTML-кодом.
Достаточно лишь добавить атрибуты n:name
. Это так просто!
protected function createComponentSignInForm(): Form
{
$form = new Form;
$form->addText('username')->setRequired();
$form->addPassword('password')->setRequired();
$form->addSubmit('send');
return $form;
}
<form n:name=signInForm class=form>
<div>
<label n:name=username>Имя пользователя: <input n:name=username size=20 autofocus></label>
</div>
<div>
<label n:name=password>Пароль: <input n:name=password></label>
</div>
<div>
<input n:name=send class="btn btn-default">
</div>
</form>
Вид результирующего HTML-кода полностью в ваших руках. Если атрибут
n:name
использовать у элементов <select>
, <button>
или
<textarea>
, их внутреннее содержимое автоматически дополнится.
Тег <form n:name>
к тому же создает локальную переменную $form
с объектом отрисовываемой формы, а закрывающий </form>
отрисовывает все неотрисованные скрытые элементы (то же самое
относится и к {form} ... {/form}
).
Однако нельзя забывать об отрисовке возможных сообщений об ошибках.
Как тех, которые были добавлены методом addError()
к отдельным
элементам (с помощью {inputError}
), так и тех, что добавлены
непосредственно к форме (их возвращает $form->getOwnErrors()
):
<form n:name=signInForm class=form>
<ul class="errors" n:ifcontent>
<li n:foreach="$form->getOwnErrors() as $error">{$error}</li>
</ul>
<div>
<label n:name=username>Имя пользователя: <input n:name=username size=20 autofocus></label>
<span class=error n:ifcontent>{inputError username}</span>
</div>
<div>
<label n:name=password>Пароль: <input n:name=password></label>
<span class=error n:ifcontent>{inputError password}</span>
</div>
<div>
<input n:name=send class="btn btn-default">
</div>
</form>
Более сложные элементы формы, такие как RadioList или CheckboxList, можно таким образом отрисовывать по отдельным элементам:
{foreach $form[gender]->getItems() as $key => $label}
<label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label>
{/foreach}
{label}
{input}
Не хотите для каждого элемента думать, какой HTML-элемент использовать
для него в шаблоне, будь то <input>
, <textarea>
и т. д.?
Решением является универсальный тег {input}
:
<form n:name=signInForm class=form>
<ul class="errors" n:ifcontent>
<li n:foreach="$form->getOwnErrors() as $error">{$error}</li>
</ul>
<div>
{label username}Имя пользователя: {input username, size: 20, autofocus: true}{/label}
{inputError username}
</div>
<div>
{label password}Пароль: {input password}{/label}
{inputError password}
</div>
<div>
{input send, class: "btn btn-default"}
</div>
</form>
Если форма использует переводчик (translator), текст внутри тегов
{label}
будет переведен.
И в этом случае более сложные элементы формы, такие как RadioList или CheckboxList, можно отрисовывать по отдельным элементам:
{foreach $form[gender]->items as $key => $label}
{label gender:$key}{input gender:$key} {$label}{/label}
{/foreach}
Для отрисовки самого <input>
в элементе Checkbox используйте
{input myCheckbox:}
. HTML-атрибуты в этом случае всегда отделяйте запятой
{input myCheckbox:, class: required}
.
{inputError}
Выводит сообщение об ошибке для элемента формы, если оно есть.
Сообщение обычно оборачивают в HTML-элемент для стилизации.
Предотвратить отрисовку пустого элемента, если сообщения нет, можно
элегантно с помощью n:ifcontent
:
<span class=error n:ifcontent>{inputError $input}</span>
Наличие ошибки можно проверить методом hasErrors()
и в зависимости
от этого установить класс родительскому элементу:
<div n:class="$form[username]->hasErrors() ? 'error'">
{input username}
{inputError username}
</div>
{form}
Теги {form signInForm}...{/form}
являются альтернативой
<form n:name="signInForm">...</form>
.
Автоматическая отрисовка
Благодаря тегам {input}
и {label}
мы можем легко создать общий
шаблон для любой формы. Он будет последовательно итерировать и
отрисовывать все ее элементы, кроме скрытых элементов, которые
отрисовываются автоматически при завершении формы тегом
</form>
. Имя отрисовываемой формы он будет ожидать в переменной
$form
.
<form n:name=$form class=form>
<ul class="errors" n:ifcontent>
<li n:foreach="$form->getOwnErrors() as $error">{$error}</li>
</ul>
<div n:foreach="$form->getControls() as $input"
n:if="$input->getOption(type) !== hidden">
{label $input /}
{input $input}
{inputError $input}
</div>
</form>
Использованные самозакрывающиеся парные теги {label .../}
отображают метки (labels), взятые из определения формы в PHP-коде.
Сохраните этот общий шаблон, например, в файле basic-form.latte
, и для
отрисовки формы достаточно его включить и передать имя (или экземпляр)
формы в параметр $form
:
{include basic-form.latte, form: signInForm}
Если при отрисовке определенной формы вы захотите изменить ее вид и, например, один элемент отрисовать иначе, то самый простой путь — подготовить в шаблоне блоки, которые можно будет впоследствии переопределить. Блоки могут также иметь динамические имена, в них можно так вставить и имя отрисовываемого элемента. Например:
...
{label $input /}
{block "input-{$input->name}"}{input $input}{/block}
...
Для элемента, например username
, будет создан блок input-username
,
который можно легко переопределить с помощью тега {embed}:
{embed basic-form.latte, form: signInForm}
{block input-username}
<span class=important>
{include parent}
</span>
{/block}
{/embed}
Альтернативно, все содержимое шаблона basic-form.latte
можно определить как блок, включая
параметр $form
:
{define basic-form, $form}
<form n:name=$form class=form>
...
</form>
{/define}
Благодаря этому его вызов станет немного проще:
{embed basic-form, signInForm}
...
{/embed}
При этом блок достаточно импортировать только в одном месте — в начале шаблона макета (layout):
{import basic-form.latte}
Особые случаи
Если вам нужно отрисовать только внутреннюю часть формы без HTML-тегов
<form>
, например, при отправке сниппетов, скройте их с помощью
атрибута n:tag-if
:
<form n:name=signInForm n:tag-if=false>
<div>
<label n:name=username>Имя пользователя: <input n:name=username></label>
{inputError username}
</div>
</form>
С отрисовкой элементов внутри контейнера формы поможет тег
{formContainer}
.
<p>Какие новости вы хотите получать:</p>
{formContainer emailNews}
<ul>
<li>{input sport} {label sport /}</li>
<li>{input science} {label science /}</li>
</ul>
{/formContainer}
Отрисовка без Latte
Самый простой способ отрисовать форму — вызвать:
$form->render();
Повлиять на вид отрисованной таким образом формы можно с помощью конфигурации Renderer и отдельных элементов.
Ручная отрисовка
Каждый элемент формы располагает методами, которые генерируют HTML-код поля формы и метки (labels). Они могут возвращать его либо как строку, либо как объект Nette\Utils\Html:
getControl(): Html|string
возвращает HTML-код элементаgetLabel($caption = null): Html|string|null
возвращает HTML-код метки, если она существует
Таким образом, форму можно отрисовывать по отдельным элементам:
<?php $form->render('begin') ?>
<?php $form->render('errors') ?>
<div>
<?= $form['name']->getLabel() ?>
<?= $form['name']->getControl() ?>
<span class=error><?= htmlspecialchars($form['name']->getError()) ?></span>
</div>
<div>
<?= $form['age']->getLabel() ?>
<?= $form['age']->getControl() ?>
<span class=error><?= htmlspecialchars($form['age']->getError()) ?></span>
</div>
// ...
<?php $form->render('end') ?>
В то время как для некоторых элементов getControl()
возвращает
единственный HTML-элемент (например, <input>
, <select>
и т.
д.), для других — целый кусок HTML-кода (CheckboxList, RadioList). В таком случае вы
можете использовать методы, которые генерируют отдельные инпуты и
метки (labels), для каждого элемента отдельно:
getControlPart($key = null): ?Html
возвращает HTML-код одного элементаgetLabelPart($key = null): ?Html
возвращает HTML-код метки одного элемента
Эти методы по историческим причинам имеют префикс get
,
но лучше было бы generate
, потому что при каждом вызове они создают и
возвращают новый Html
элемент.
Renderer
Это объект, обеспечивающий отрисовку формы. Его можно установить
методом $form->setRenderer
. Ему передается управление при вызове
метода $form->render()
.
Если мы не установим собственный рендерер, будет использован рендерер по умолчанию Nette\Forms\Rendering\DefaultFormRenderer. Он отрисует элементы формы в виде HTML-таблицы. Вывод выглядит так:
<table>
<tr class="required">
<th><label class="required" for="frm-name">Имя:</label></th>
<td><input type="text" class="text" name="name" id="frm-name" required value=""></td>
</tr>
<tr class="required">
<th><label class="required" for="frm-age">Возраст:</label></th>
<td><input type="text" class="text" name="age" id="frm-age" required value=""></td>
</tr>
<tr>
<th><label>Пол:</label></th>
...
Использовать таблицу для каркаса формы или нет — спорный вопрос, и
многие веб-дизайнеры предпочитают другую разметку. Например, список
определений. Поэтому переконфигурируем DefaultFormRenderer
так, чтобы он
отрисовал форму в виде списка. Конфигурация выполняется путем
редактирования массива $wrappers.
Первый индекс всегда представляет область, а второй — ее атрибут.
Отдельные области показаны на рисунке:

По умолчанию группа элементов controls
обернута таблицей
<table>
, каждый pair
представляет строку таблицы
<tr>
, а пара label
и control
— ячейки <th>
и
<td>
. Теперь изменим оборачивающие элементы. Область
controls
вложим в контейнер <dl>
, область pair
оставим
без контейнера, label
вложим в <dt>
и, наконец, control
обернем тегами <dd>
:
$renderer = $form->getRenderer();
$renderer->wrappers['controls']['container'] = 'dl';
$renderer->wrappers['pair']['container'] = null;
$renderer->wrappers['label']['container'] = 'dt';
$renderer->wrappers['control']['container'] = 'dd';
$form->render();
Результатом является этот HTML-код:
<dl>
<dt><label class="required" for="frm-name">Имя:</label></dt>
<dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd>
<dt><label class="required" for="frm-age">Возраст:</label></dt>
<dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd>
<dt><label>Пол:</label></dt>
...
</dl>
В массиве wrappers можно повлиять на целый ряд других атрибутов:
- добавлять CSS-классы отдельным типам элементов формы
- различать CSS-классом четные и нечетные строки
- визуально отличать обязательные и необязательные элементы
- определять, будут ли сообщения об ошибках отображаться непосредственно у элементов или над формой
Options
Поведением Renderer можно управлять также путем установки options для отдельных элементов формы. Так можно установить надпись, которая выведется рядом с полем ввода:
$form->addText('phone', 'Номер:')
->setOption('description', 'Этот номер останется скрытым');
Если мы хотим поместить в него HTML-содержимое, используем класс Html
use Nette\Utils\Html;
$form->addText('phone', 'Номер:')
->setOption('description', Html::el('p')
->setHtml('<a href="...">Условия хранения вашего номера</a>')
);
Html-элемент можно использовать и вместо метки (label):
$form->addCheckbox('conditions', $label)
.
Группировка элементов
Renderer позволяет группировать элементы в визуальные группы (fieldset):
$form->addGroup('Личные данные');
После создания новой группы она становится активной, и каждый вновь добавленный элемент одновременно добавляется и в нее. Так что форму можно строить таким образом:
$form = new Form;
$form->addGroup('Личные данные');
$form->addText('name', 'Ваше имя:');
$form->addInteger('age', 'Ваш возраст:');
$form->addEmail('email', 'Email:');
$form->addGroup('Адрес доставки');
$form->addCheckbox('send', 'Отправить по адресу');
$form->addText('street', 'Улица:');
$form->addText('city', 'Город:');
$form->addSelect('country', 'Страна:', $countries);
Renderer сначала отрисовывает группы, а затем элементы, которые не принадлежат ни к какой группе.
Поддержка Bootstrap
В примерах вы найдете примеры, как сконфигурировать Renderer для Twitter Bootstrap 2, Bootstrap 3 и Bootstrap 4
HTML-атрибуты
Для установки любых HTML-атрибутов элементов формы используем метод
setHtmlAttribute(string $name, $value = true)
:
$form->addInteger('number', 'Номер:')
->setHtmlAttribute('class', 'big-number');
$form->addSelect('rank', 'Сортировать по:', ['цене', 'названию'])
->setHtmlAttribute('onchange', 'submit()'); // отправить при изменении
// Для установки атрибутов самой <form>
$form->setHtmlAttribute('id', 'myForm');
Спецификация типа элемента:
$form->addText('tel', 'Ваш телефон:')
->setHtmlType('tel')
->setHtmlAttribute('placeholder', 'введите телефон');
Установка типа и других атрибутов служит только для визуальных целей. Проверка правильности ввода должна происходить на сервере, что вы обеспечите выбором подходящего элемента формы и указанием правил валидации.
Отдельным элементам в списках radio или checkbox мы можем установить
HTML-атрибут с различными значениями для каждого из них. Обратите
внимание на двоеточие после style:
, которое обеспечит выбор
значения по ключу:
$colors = ['r' => 'красный', 'g' => 'зеленый', 'b' => 'синий'];
$styles = ['r' => 'background:red', 'g' => 'background:green'];
$form->addCheckboxList('colors', 'Цвета:', $colors)
->setHtmlAttribute('style:', $styles);
Выведет:
<label><input type="checkbox" name="colors[]" style="background:red" value="r">красный</label>
<label><input type="checkbox" name="colors[]" style="background:green" value="g">зеленый</label>
<label><input type="checkbox" name="colors[]" value="b">синий</label>
Для установки логических атрибутов, таких как readonly
, мы можем
использовать запись с вопросительным знаком:
$form->addCheckboxList('colors', 'Цвета:', $colors)
->setHtmlAttribute('readonly?', 'r'); // для нескольких ключей используйте массив, например ['r', 'g']
Выведет:
<label><input type="checkbox" name="colors[]" readonly value="r">красный</label>
<label><input type="checkbox" name="colors[]" value="g">зеленый</label>
<label><input type="checkbox" name="colors[]" value="b">синий</label>
В случае селектбоксов метод setHtmlAttribute()
устанавливает атрибуты
элемента <select>
. Если мы хотим установить атрибуты отдельным
<option>
, используем метод setOptionAttribute()
. Работают и записи
с двоеточием и вопросительным знаком, указанные выше:
$form->addSelect('colors', 'Цвета:', $colors)
->setOptionAttribute('style:', $styles);
Выведет:
<select name="colors">
<option value="r" style="background:red">красный</option>
<option value="g" style="background:green">зеленый</option>
<option value="b">синий</option>
</select>
Прототипы
Альтернативный способ установки HTML-атрибутов заключается в
изменении прототипа (prototype), из которого генерируется HTML-элемент.
Прототипом является объект Html
, и его возвращает метод
getControlPrototype()
:
$input = $form->addInteger('number', 'Номер:');
$html = $input->getControlPrototype(); // <input>
$html->class('big-number'); // <input class="big-number">
Этим способом можно модифицировать и прототип метки (label), который
возвращает getLabelPrototype()
:
$html = $input->getLabelPrototype(); // <label>
$html->class('distinctive'); // <label class="distinctive">
У элементов Checkbox, CheckboxList и RadioList вы можете влиять на прототип
элемента, который оборачивает весь элемент. Его возвращает
getContainerPrototype()
. В исходном состоянии это «пустой» элемент, так что
ничего не отрисовывается, но тем, что мы установим ему имя, он будет
отрисовываться:
$input = $form->addCheckbox('send');
$html = $input->getContainerPrototype();
$html->setName('div'); // <div>
$html->class('check'); // <div class="check">
echo $input->getControl();
// <div class="check"><label><input type="checkbox" name="send"></label></div>
В случае CheckboxList и RadioList можно влиять и на прототип разделителя
отдельных элементов, который возвращает метод getSeparatorPrototype()
. В
исходном состоянии это элемент <br>
. Если вы измените его на
парный элемент, он будет оборачивать отдельные элементы вместо
разделения. А далее можно влиять на прототип HTML-элемента метки у
отдельных элементов, который возвращает getItemLabelPrototype()
.
Перевод
Если вы программируете многоязычное приложение, вам, вероятно, потребуется отрисовать форму в различных языковых версиях. Nette Framework для этой цели определяет интерфейс для перевода Nette\Localization\Translator. В Nette нет реализации по умолчанию, вы можете выбрать в соответствии со своими потребностями из нескольких готовых решений, которые найдете на Componette. В их документации вы узнаете, как конфигурировать переводчик (translator).
Формы поддерживают вывод текстов через переводчик (translator). Передадим
его им с помощью метода setTranslator()
:
$form->setTranslator($translator);
С этого момента не только все метки (labels), но и все сообщения об ошибках или элементы select box будут переведены на другой язык.
При этом для отдельных элементов формы можно установить другой
переводчик или полностью отключить перевод, установив значение
null
:
$form->addSelect('carModel', 'Модель:', $cars)
->setTranslator(null);
Для правил валидации переводчику (translator) передаются также специфические параметры, например, у правила:
$form->addPassword('password', 'Пароль:')
->addRule($form::MinLength, 'Пароль должен содержать не менее %d символов', 8);
вызывается переводчик (translator) с этими параметрами:
$translator->translate('Пароль должен содержать не менее %d символов', 8);
и, следовательно, он может выбрать правильную форму множественного
числа для слова символов
в зависимости от числа.
Событие onRender
Непосредственно перед отрисовкой формы мы можем вызвать наш код. Он
может, например, дополнить элементы формы HTML-классами для правильного
отображения. Код добавим в массив onRender
:
$form->onRender[] = function ($form) {
BootstrapCSS::initialize($form);
};