Рендеринг форм
Внешний вид форм может быть очень разнообразным. На практике мы можем
столкнуться с двумя крайностями. С одной стороны, в приложении
необходимо отобразить ряд форм, визуально похожих друг на друга, и мы
ценим простоту визуализации без шаблона с помощью $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}
будет переведен.
Опять же, более сложные элементы формы, такие как 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 .../}
отображают метки, поступающие из определения формы в 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
помогает при отрисовке вводимых данных внутри
контейнера формы.
<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" required 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" required 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" required value=""></dd>
<dt><label class="required" for="frm-age">Age:</label></dt>
<dd><input type="text" class="text" name="age" id="frm-age" required 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>
Для задания булевых атрибутов, таких как readonly
, можно
использовать обозначение с вопросительным знаком:
$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');
$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', 'Model:', $cars)
->setTranslator(null);
Для правил валидации переводчику также передаются определенные параметры, например, для правила:
$form->addPassword('password', 'Password:')
->addRule($form::MinLength, 'Password has to be at least %d characters long', 8)
транслятор вызывается со следующими параметрами:
$translator->translate('Password has to be at least %d characters long', 8);
и таким образом может выбрать правильную форму множественного числа
для слова characters
по счету.
Событие onRender
Непосредственно перед отображением формы мы можем вызвать наш код. Это может, например, добавить HTML-классы к элементам формы для правильного отображения. Добавим код в массив ‘onRender’:
$form->onRender[] = function ($form) {
BootstrapCSS::initialize($form);
};