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

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

Рендеринг без латте

Найпростіший спосіб відобразити форму – викликати:

$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);
};
версію: 4.0