Відображення форм

Зовнішній вигляд форм може бути дуже різноманітним. На практиці ми можемо зіткнутися з двома крайнощами. З одного боку, існує потреба відображати в додатку низку форм, які візуально схожі одна на одну, як дві краплі води, і ми оцінимо легкість відображення без шаблону за допомогою $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>Username: <input n:name=username size=20 autofocus></label>
	</div>
	<div>
		<label n:name=password>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>Username: <input n:name=username size=20 autofocus></label>
		<span class=error n:ifcontent>{inputError username}</span>
	</div>
	<div>
		<label n:name=password>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}Username: {input username, size: 20, autofocus: true}{/label}
		{inputError username}
	</div>
	<div>
		{label password}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}

При цьому блок достатньо імпортувати лише в одному місці, а саме на початку шаблону layout:

{import basic-form.latte}

Спеціальні випадки

Якщо потрібно відобразити лише внутрішню частину форми без HTML-тегів <form>, наприклад, при надсиланні сніпетів, приховайте їх за допомогою атрибута n:tag-if:

<form n:name=signInForm n:tag-if=false>
	<div>
		<label n:name=username>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-код поля форми та мітки. Вони можуть повертати його або як рядок, або як об'єкт 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.

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 можна використовувати також замість мітки: $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>

У випадку selectbox метод 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);

З цього моменту не тільки всі мітки, але й усі повідомлення про помилки або пункти select box перекладаються іншою мовою.

Для окремих елементів форми при цьому можна встановити інший перекладач або повністю вимкнути переклад значенням 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.0