Отрисовка форм

Внешний вид форм может быть очень разным. На практике мы можем столкнуться с двумя крайностями. С одной стороны, существует необходимость отрисовывать в приложении множество форм, визуально похожих друг на друга как две капли воды, и мы оценим простую отрисовку без шаблона с помощью $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);
};
версия: 4.0