Рендеринг форм

Внешний вид форм может сильно отличаться. На самом деле, есть две крайности. Первая — это необходимость заново создавать набор очень похожих форм, практически не прилагая усилий. Как правило, это формы в бэкенде.

С другой стороны — крошечные сладкие формы, каждая из которых — произведение искусства. Их макет лучше всего написать на языке HTML. Конечно, помимо этих крайностей, существует множество форм, находящихся между ними.

Latte

Система шаблонов Latte в корне облегчает отрисовку форм и их элементов. Сначала мы покажем, как отрисовывать формы вручную, элемент за элементом, чтобы получить полный контроль над кодом. Позже мы покажем, как автоматизировать такой рендеринг.

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

Предложение кода {formPrint}

Вы можете сгенерировать подобный код Latte для формы, используя тег {formPrint}. Если вы поместите его в шаблон, вы увидите проект кода вместо обычного рендеринга. Затем просто выделите его и скопируйте в свой проект.

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

Если форма использует переводчик, то текст внутри тегов {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>

Автоматический рендеринг

С помощью тегов {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 .../} отображают метки, поступающие из определения формы в 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}

Вам нужно будет импортировать блок только в одном месте, в начале шаблона макета:

{import basic-form.latte}

Особые случаи

Если вам нужно отобразить только внутреннее содержимое формы без <form> & </form> HTML тегов, например, в AJAX запросе, вы можете открывать и закрывать форму с помощью {formContext} … {/formContext}. Он работает аналогично {form} в логическом смысле, здесь он позволяет вам использовать другие теги для рисования элементов формы, но в то же время он ничего не рисует.

{formContext signForm}
	<div>
		<label n:name=username>Имя пользователя: <input n:name=username></label>
		{inputError username}
	</div>
{/formContext}

Тег formContainer помогает при отрисовке вводимых данных внутри контейнера формы.

<p>Which news you wish to receive:</p>

{formContainer emailNews}
<ul>
	<li>{input sport} {label sport /}</li>
	<li>{input science} {label science /}</li>
</ul>
{/formContainer}

Без Latte

Самый простой способ отобразить форму – вызвать:

$form->render();

Внешний вид отрисованной формы можно изменить, настроив Renderer и отдельные элементы управления.

Рендеринг вручную

Каждый элемент формы имеет методы, которые генерируют HTML код для поля и метки формы. Они могут возвращать его в виде строки или объекта 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). В этом случае можно использовать методы, генерирующие отдельные входы и метки, для каждого элемента отдельно:

  • getControlPart($key = null): ?Html возвращает HTML-код одного элемента.
  • getLabelPart($key = null): ?Html возвращает HTML-код для метки отдельного элемента

Эти методы имеют префикс get по историческим причинам, но generate было бы лучше, так как он создает и возвращает новый элемент Html при каждом вызове.

Рендерер

Это объект, обеспечивающий рендеринг формы. Он может быть установлен методом $form->setRenderer. Ему передается управление при вызове метода $form->render().

Если мы не зададим пользовательский рендерер, будет использоваться рендерер по умолчанию Nette\Forms\Rendering\DefaultFormRenderer. В результате элементы формы будут отображены в виде HTML-таблицы. Вывод выглядит следующим образом:

<table>
<tr class="required">
	<th><label class="required" for="frm-name">Name:</label></th>

	<td><input type="text" class="text" name="name" id="frm-name" value=""></td>
</tr>

<tr class="required">
	<th><label class="required" for="frm-age">Age:</label></th>

	<td><input type="text" class="text" name="age" id="frm-age" value=""></td>
</tr>

<tr>
	<th><label>Gender:</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();

В результате получится следующий фрагмент:

<dl>
	<dt><label class="required" for="frm-name">Name:</label></dt>

	<dd><input type="text" class="text" name="name" id="frm-name" value=""></dd>


	<dt><label class="required" for="frm-age">Age:</label></dt>

	<dd><input type="text" class="text" name="age" id="frm-age" value=""></dd>


	<dt><label>Gender:</label></dt>
	...
</dl>

Обертки могут влиять на многие атрибуты. Например:

  • добавить специальные классы CSS к каждому вводу формы
  • различать четные и нечетные строки
  • по-разному рисовать обязательные и необязательные элементы
  • установить, будут ли сообщения об ошибках показываться над формой или рядом с каждым элементом

Опции

Поведением Renderer также можно управлять, устанавливая опции для отдельных элементов формы. Таким образом, вы можете установить всплывающую подсказку, которая будет отображаться рядом с полем ввода:

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

Группировка входов

Поля ввода можно объединить в наборы визуальных полей, создав группу:

$form->addGroup('Персональные данные');

Создание новой группы активирует её — все добавленные далее элементы добавляются в эту группу. Вы можете построить форму следующим образом:

$form = new Form;
$form->addGroup('Персональные данные');
$form->addText('name', 'Ваше имя:');
$form->addInteger('age', 'Ваш возраст:');
$form->addEmail('email', 'Имейл:');

$form->addGroup('Адрес доставки');
$form->addCheckbox('send', 'Отправить по адресу');
$form->addText('street', 'Улица:');
$form->addText('city', 'Город:');
$form->addSelect('country', 'Страна:', $countries);

Поддержка Bootstrap

Вы можете найти примеры настройки рендерера для Twitter Bootstrap 2, Bootstrap 3 и Bootstrap 4

Атрибуты HTML

Вы можете установить любые атрибуты HTML для элементов управления формы с помощью setHtmlAttribute(string $name, $value = true):

$form->addInteger('number', 'Количество:')
	->setHtmlAttribute('class', 'bigNumbers');

$form->addSelect('rank', 'Заказать:', ['price', 'name'])
	->setHtmlAttribute('onchange', 'submit()'); // вызывает JS-функцию submit() при изменении


// применяется на <form>
$form->setHtmlAttribute('id', 'myForm');

Установка типа входа:

$form->addText('tel', 'Ваш телефон:')
	->setHtmlType('tel')
	->setHtmlAttribute('placeholder', 'Пожалуйста, заполните ваш телефон');

Мы можем установить HTML-атрибут для отдельных элементов в списках радио- или чекбоксов с разными значениями для каждого из них. Обратите внимание на двоеточие после style:, чтобы убедиться, что значение выбирается по ключу:

$colors = ['r' => 'red', 'g' => 'green', 'b' => 'blue'];
$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">red</label>
<label><input type="checkbox" name="colors[]" style="background:green" value="g">green</label>
<label><input type="checkbox" name="colors[]" value="b">blue</label>

Для логического атрибута HTML (который не имеет значения, например, readonly) можно использовать вопросительный знак:

$colors = ['r' => 'red', 'g' => 'green', 'b' => 'blue'];
$form->addCheckboxList('colors', 'Цвета:', $colors)
	->setHtmlAttribute('readonly?', 'r'); // использовать массив для нескольких ключей, например ['r', 'g']

Отображается как:

<label><input type="checkbox" name="colors[]" readonly value="r">red</label>
<label><input type="checkbox" name="colors[]" value="g">green</label>
<label><input type="checkbox" name="colors[]" value="b">blue</label>

Для селекбоксов метод setHtmlAttribute() устанавливает атрибуты элемента <select>. Если мы хотим установить атрибуты для каждого <option>, мы будем использовать метод setOptionAttribute(). Кроме того, двоеточие и вопросительный знак, использованные выше, работают:

$form->addSelect('colors', 'Цвета:', $colors)
	->setOptionAttribute('style:', $styles);

Отображается как:

<select name="colors">
	<option value="r" style="background:red">red</option>
	<option value="g" style="background:green">green</option>
	<option value="b">blue</option>
</select>

Прототипы

Альтернативным способом установки атрибутов HTML является изменение шаблона, на основе которого генерируется элемент HTML. Шаблон является объектом Html и возвращается методом getControlPrototype():

$input = $form->addInteger('number');
$html = $input->getControlPrototype(); // <input>
$html->class('big-number');            // <input class="big-number">

Шаблон метки, возвращаемый методом getLabelPrototype(), также может быть изменен таким образом:

$html = $input->getLabelPrototype(); // <label>
$html->class('distinctive');         // <label class="distinctive">

Для элементов Checkbox, CheckboxList и RadioList вы можете повлиять на шаблон элемента, который оборачивает элемент. Его возвращает getContainerPrototype(). По умолчанию это “пустой” элемент, поэтому ничего не отображается, но если дать ему имя, он будет отображаться:

$input = $form->addCheckbox('send');
echo $input->getControl();
// <label><input type="checkbox" name="send"></label>

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

Событие onRender

Непосредственно перед отображением формы мы можем вызвать наш код. Это может, например, добавить HTML-классы к элементам формы для правильного отображения. Добавим код в массив ‘onRender’:

$form->onRender[] = function ($form) {
	BootstrapCSS::initialize($form);
};