[Go to site: main page, start]

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

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

Если форма использует переводчик, метки, отрисованные из определения формы (например, {label username /}), переводятся. Текст, написанный прямо между тегами {label} и {/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>. Любые аргументы отделяйте от имени запятой: {form signInForm, class: foo}.

Ключевое слово scope, поставленное перед именем, только помещает форму в стек (чтобы {input}, {label} и прочие к ней привязывались), но тег <form> не отрисовывает. Это удобно для отрисовки части формы, например в сниппете. Если форма уже активна, имя разрешается относительно неё, так что {form scope} заменяет и {formContainer}:

{form scope signInForm}
	{input username}
{/form}

Ключевое слово detached отрисовывает пустой <form></form> и привязывает к нему каждый элемент через HTML-атрибут form. Это позволяет поместить форму внутрь другой формы, чего HTML иначе не допускает. У отделённой формы должен быть HTML-атрибут id, который порождается автоматически, когда вы даёте ей имя (как outerForm ниже):

{form detached outerForm}
	...
{/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 .../} выводят метки, происходящие из определения формы в 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}

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

Если вам нужно отрисовать только внутреннюю часть формы без 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} или более новый {form scope}.

<p>Какие новости вы хотите получать:</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('ownerrors') ?>

<div>
	<?= $form['name']->getLabel() ?>
	<?= $form['name']->getControl() ?>
	<span class=error><?= htmlspecialchars((string) $form['name']->getError()) ?></span>
</div>

<div>
	<?= $form['age']->getLabel() ?>
	<?= $form['age']->getControl() ?>
	<span class=error><?= htmlspecialchars((string) $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.

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-классами
  • визуально отличать обязательные пункты от необязательных
  • определять, показываются ли сообщения об ошибках прямо рядом с элементами или над формой

Параметры

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

$form->addText('phone', 'Номер:')
	->setOption('description', 'Этот номер останется скрытым');

Если мы хотим разместить в нём HTML-содержимое, воспользуемся классом Html:

use Nette\Utils\Html;

$form->addText('phone', 'Телефон:')
	->setOption('description', Html::el('p')
		->setHtml('<a href="...">Условия использования.</a>')
	);

Элемент Html можно использовать и вместо метки: $form->addCheckbox('conditions', $label).

Группировка элементов

Отрисовщик позволяет группировать элементы в визуальные группы (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);

Отрисовщик рисует сначала группы, а затем элементы, которые ни в какую группу не входят.

Поддержка Bootstrap

В каталоге examples вы найдёте примеры того, как настроить отрисовщик для 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', 'Заполните, пожалуйста, ваш телефон');

Задание типа и других атрибутов служит только для визуальных целей. Проверка правильности ввода должна происходить на стороне сервера, что вы обеспечиваете выбором подходящего элемента формы и указанием правил проверки.

Отдельным пунктам радиосписков и списков флажков можно задать 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-атрибуты – изменить шаблон, из которого порождается 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');
$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. В их документации написано, как настроить переводчик.

Формы поддерживают вывод текстов через переводчик. Передаём его методом setTranslator():

$form->setTranslator($translator);

С этого момента на целевой язык будут переводиться не только все метки, но и все сообщения об ошибках, пункты выпадающих списков и подсказки в полях.

Отдельным элементам формы можно задать другой переводчик или полностью отключить перевод, задав значение null:

$form->addSelect('carModel', 'Модель:', $cars)
	->setTranslator(null);

Для правил проверки переводчику передаются и конкретные параметры. Например, для правила:

$form->addPassword('password', 'Пароль:')
	->addRule($form::MinLength, 'Пароль должен быть длиной не менее %d символов', 8);

переводчик вызывается с такими параметрами:

$translator->translate('Пароль должен быть длиной не менее %d символов', 8);

и, стало быть, может по количеству выбрать правильную форму множественного числа слова символов.

Событие onRender

Прямо перед отрисовкой формы мы можем дать вызвать свой код. Он может, например, добавить элементам формы HTML-классы для правильного отображения. Код добавляем в массив onRender:

$form->onRender[] = function ($form) {
	BootstrapCSS::initialize($form);
};
версия: 4.x