Рендеринг форм
Зовнішній вигляд форм може бути дуже різноманітним. На практиці ми
можемо зіткнутися з двома крайнощами. З одного боку, є потреба
відрендерити серію форм у додатку, які візуально схожі одна на одну, і
ми цінуємо простий рендеринг без шаблону за допомогою
$form->render()
. Зазвичай це стосується адміністративних
інтерфейсів.
З іншого боку, існують різні форми, де кожна з них унікальна. Їх зовнішній вигляд найкраще описати за допомогою мови HTML в шаблоні. І, звичайно, крім обох згаданих крайнощів, ми зустрінемо багато форм, які знаходяться десь посередині.
Візуалізація за допомогою Latte
Система шаблонів Latte докорінно полегшує відтворення форм та їхніх елементів. Спочатку ми покажемо, як відтворювати форми вручну, елемент за елементом, щоб отримати повний контроль над кодом. Пізніше ми покажемо, як автоматизувати такий рендеринг.
{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}
Пропозиція коду {formPrint}
Ви можете згенерувати подібний код Latte для форми, використовуючи тег
{formPrint}
. Якщо ви помістите його в шаблон, ви побачите проєкт коду
замість звичайного рендерингу. Потім просто виділіть його та
скопіюйте у свій проект.
{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}
Особливі випадки
Якщо вам потрібно відобразити тільки внутрішній вміст форми без
<form>
& </form>
HTML тегів, наприклад, в AJAX запиті, ви
можете відкривати і закривати форму за допомогою
{formContext} … {/formContext}
. Він працює аналогічно {form}
в логічному
сенсі, тут він дозволяє вам використовувати інші теги для малювання
елементів форми, але в той же час він нічого не малює.
{formContext signForm}
<div>
<label n:name=username>Имя пользователя: <input n:name=username></label>
{inputError username}
</div>
{/formContext}
Тег 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>
Для логічного атрибута HTML (який не має значення, наприклад,
readonly
) можна використовувати знак питання:
$colors = ['r' => 'red', 'g' => 'green', 'b' => 'blue'];
$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');
echo $input->getControl();
// <label><input type="checkbox" name="send"></label>
$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);
};