Nette Documentation Preview

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

Внешний вид форм может быть очень разным. На практике мы можем столкнуться с двумя крайностями. С одной стороны, существует необходимость отрисовывать в приложении множество форм, визуально похожих друг на друга как две капли воды, и мы оценим простую отрисовку без шаблона с помощью `$form->render()`. Обычно это относится к административным интерфейсам.

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


Отрисовка с помощью Latte
=========================

[Система шаблонов Latte |latte:] существенно упрощает отрисовку форм и их элементов. Сначала мы покажем, как отрисовывать формы вручную по отдельным элементам и тем самым получить полный контроль над кодом. Позже мы покажем, как можно такую отрисовку [автоматизировать |#Automatické vykreslování].

Проект Latte-шаблона для формы можно сгенерировать с помощью метода `Nette\Forms\Blueprint::latte($form)`, который выведет его на страницу браузера. Затем достаточно кликнуть, чтобы выделить код, и скопировать его в проект. .{data-version:3.1.15}


`{control}`
-----------

Самый простой способ отрисовать форму — написать в шаблоне:

```latte
{control signInForm}
```

Повлиять на вид отрисованной таким образом формы можно с помощью конфигурации [Renderer |#Renderer] и [отдельных элементов |#HTML atributy].


`n:name`
--------

Определение формы в PHP-коде можно очень легко связать с HTML-кодом. Достаточно лишь добавить атрибуты `n:name`. Это так просто!

```php
protected function createComponentSignInForm(): Form
{
	$form = new Form;
	$form->addText('username')->setRequired();
	$form->addPassword('password')->setRequired();
	$form->addSubmit('send');
	return $form;
}
```

```latte
<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()`):

```latte
<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, можно таким образом отрисовывать по отдельным элементам:

```latte
{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}`:

```latte
<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, можно отрисовывать по отдельным элементам:

```latte
{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`:

```latte
<span class=error n:ifcontent>{inputError $input}</span>
```

Наличие ошибки можно проверить методом `hasErrors()` и в зависимости от этого установить класс родительскому элементу:

```latte
<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`.

```latte
<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`:

```latte
{include basic-form.latte, form: signInForm}
```

Если при отрисовке определенной формы вы захотите изменить ее вид и, например, один элемент отрисовать иначе, то самый простой путь — подготовить в шаблоне блоки, которые можно будет впоследствии переопределить. Блоки могут также иметь [динамические имена |latte:template-inheritance#dynamicke-nazvy-bloku], в них можно так вставить и имя отрисовываемого элемента. Например:

```latte
...
	{label $input /}
	{block "input-{$input->name}"}{input $input}{/block}
...
```

Для элемента, например `username`, будет создан блок `input-username`, который можно легко переопределить с помощью тега [{embed} |latte:template-inheritance#jednotkova-dedicnost]:

```latte
{embed basic-form.latte, form: signInForm}
	{block input-username}
		<span class=important>
			{include parent}
		</span>
	{/block}
{/embed}
```

Альтернативно, все содержимое шаблона `basic-form.latte` можно [определить |latte:template-inheritance#definice] как блок, включая параметр `$form`:

```latte
{define basic-form, $form}
	<form n:name=$form class=form>
		...
	</form>
{/define}
```

Благодаря этому его вызов станет немного проще:

```latte
{embed basic-form, signInForm}
	...
{/embed}
```

При этом блок достаточно импортировать только в одном месте — в начале шаблона макета (layout):

```latte
{import basic-form.latte}
```


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

Если вам нужно отрисовать только внутреннюю часть формы без HTML-тегов `<form>`, например, при отправке сниппетов, скройте их с помощью атрибута `n:tag-if`:

```latte
<form n:name=signInForm n:tag-if=false>
	<div>
		<label n:name=username>Имя пользователя: <input n:name=username></label>
		{inputError username}
	</div>
</form>
```

С отрисовкой элементов внутри контейнера формы поможет тег `{formContainer}`.

```latte
<p>Какие новости вы хотите получать:</p>

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


Отрисовка без Latte
===================

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

```php
$form->render();
```

Повлиять на вид отрисованной таким образом формы можно с помощью конфигурации [Renderer |#Renderer] и [отдельных элементов |#HTML atributy].


Ручная отрисовка
----------------

Каждый элемент формы располагает методами, которые генерируют HTML-код поля формы и метки (labels). Они могут возвращать его либо как строку, либо как объект [Nette\Utils\Html |utils:html-elements]:

- `getControl(): Html|string` возвращает HTML-код элемента
- `getLabel($caption = null): Html|string|null` возвращает HTML-код метки, если она существует

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

```php
<?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-код метки одного элемента

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


Renderer
========

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

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

```latte
<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 |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. Первый индекс всегда представляет область, а второй — ее атрибут. Отдельные области показаны на рисунке:

[* defaultformrenderer.webp *]

По умолчанию группа элементов `controls` обернута таблицей `<table>`, каждый `pair` представляет строку таблицы `<tr>`, а пара `label` и `control` — ячейки `<th>` и `<td>`. Теперь изменим оборачивающие элементы. Область `controls` вложим в контейнер `<dl>`, область `pair` оставим без контейнера, `label` вложим в `<dt>` и, наконец, `control` обернем тегами `<dd>`:

```php
$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-код:

```latte
<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* для отдельных элементов формы. Так можно установить надпись, которая выведется рядом с полем ввода:

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

Если мы хотим поместить в него HTML-содержимое, используем класс [Html |utils:html-elements]

```php
use Nette\Utils\Html;

$form->addText('phone', 'Номер:')
	->setOption('description', Html::el('p')
		->setHtml('<a href="...">Условия хранения вашего номера</a>')
	);
```

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


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

Renderer позволяет группировать элементы в визуальные группы (fieldset):

```php
$form->addGroup('Личные данные');
```

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

```php
$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
-------------------

[В примерах |https://github.com/nette/forms/tree/master/examples] вы найдете примеры, как сконфигурировать Renderer для [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] и [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php]


HTML-атрибуты
=============

Для установки любых HTML-атрибутов элементов формы используем метод `setHtmlAttribute(string $name, $value = true)`:

```php
$form->addInteger('number', 'Номер:')
	->setHtmlAttribute('class', 'big-number');

$form->addSelect('rank', 'Сортировать по:', ['цене', 'названию'])
	->setHtmlAttribute('onchange', 'submit()'); // отправить при изменении


// Для установки атрибутов самой <form>
$form->setHtmlAttribute('id', 'myForm');
```

Спецификация типа элемента:

```php
$form->addText('tel', 'Ваш телефон:')
	->setHtmlType('tel')
	->setHtmlAttribute('placeholder', 'введите телефон');
```

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

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

```php
$colors = ['r' => 'красный', 'g' => 'зеленый', 'b' => 'синий'];
$styles = ['r' => 'background:red', 'g' => 'background:green'];
$form->addCheckboxList('colors', 'Цвета:', $colors)
	->setHtmlAttribute('style:', $styles);
```

Выведет:

```latte
<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`, мы можем использовать запись с вопросительным знаком:

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

Выведет:

```latte
<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()`. Работают и записи с двоеточием и вопросительным знаком, указанные выше:

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

Выведет:

```latte
<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()`:

```php
$input = $form->addInteger('number', 'Номер:');
$html = $input->getControlPrototype(); // <input>
$html->class('big-number');            // <input class="big-number">
```

Этим способом можно модифицировать и прототип метки (label), который возвращает `getLabelPrototype()`:

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

У элементов Checkbox, CheckboxList и RadioList вы можете влиять на прототип элемента, который оборачивает весь элемент. Его возвращает `getContainerPrototype()`. В исходном состоянии это «пустой» элемент, так что ничего не отрисовывается, но тем, что мы установим ему имя, он будет отрисовываться:

```php
$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 для этой цели определяет интерфейс для перевода [api:Nette\Localization\Translator]. В Nette нет реализации по умолчанию, вы можете выбрать в соответствии со своими потребностями из нескольких готовых решений, которые найдете на [Componette |https://componette.org/search/localization]. В их документации вы узнаете, как конфигурировать переводчик (translator).

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

```php
$form->setTranslator($translator);
```

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

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

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

Для [правил валидации |validation] переводчику (translator) передаются также специфические параметры, например, у правила:

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

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

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

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


Событие onRender
================

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

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

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

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