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

Външният вид на формите може да бъде много разнообразен. На практика можем да срещнем две крайности. От една страна, стои нуждата в приложението да се рендират редица форми, които визуално си приличат като две капки вода, и ще оценим лесното рендиране без шаблон с помощта на $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>Кои новини желаете да получавате:</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). В такъв случай можете да използвате методи, които генерират отделни input-и и етикети, за всеки елемент поотделно:

  • getControlPart($key = null): ?Html връща HTML кода на един елемент
  • getLabelPart($key = null): ?Html връща HTML кода на етикета на един елемент

Тези методи имат префикс get по исторически причини, но generate би бил по-добър, тъй като при всяко извикване създава и връща нов елемент Html.

Renderer

Това е обект, осигуряващ рендирането на формата. Той може да бъде зададен с метода $form->setRenderer. Контролът му се предава при извикване на метода $form->render().

Ако не зададем собствен renderer, ще бъде използван renderer-ът по подразбиране 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>
	...

Дали да се използва или не таблица за скелета на формата е спорно и много уеб дизайнери предпочитат друг markup. Например дефиниционен списък. Затова ще преконфигурираме 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