Vykreslování formulářů

Vzhled formulářů může být velmi různorodý. V praxi můžeme narazit na dva extrémy. Na jedné straně stojí potřeba v aplikaci vykreslovat řadu formulářů, které jsou si vizuálně podobné jako vejce vejci, a oceníme snadné vykreslení bez šablony pomocí $form->render(). Jde obvykle o případ administračních rozhraní.

Na druhé straně tu jsou rozmanité formuláře, kde platí: co kus, to originál. Jejich podobu nejlépe popíšeme jazykem HTML v šabloně formuláře. A samozřejmě kromě obou zmíněných extrémů narazíme na spoustu formulářů, které se pohybují někde mezi.

Vykreslení pomocí Latte

Šablonovací sytém Latte zásadně usnadňuje vykreslení formulářů a jejich prvků. Nejprve si ukážeme, jak formuláře vykreslovat ručně po jednotlivých prvcích a tím získat plnou kontrolu nad kódem. Později si ukážeme, jak lze takové vykreslování zautomatizovat.

Návrh Latte šablony formuláře si můžete nechat vygenerovat pomocí metody Nette\Forms\Blueprint::latte($form), která jej vypíše do stránky prohlížeče. Kód pak stačí kliknutím označit a zkopírovat do projektu.

{control}

Nejjednodušší způsob, jak vykreslit formulář, je napsat v šabloně:

{control signInForm}

Ovlivnit podobu takto vykresleného formuláře lze konfigurací Rendereru a jednotlivých prvků.

n:name

Definici formuláře v PHP kódu lze nesmírně snadno provázat s HTML kódem. Stačí jen doplnit atributy n:name. Tak je to snadné!

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>

Podobu výsledného HTML kódu máte plně ve svých rukou. Pokud atribut n:name použijete u elementů <select>, <button> nebo <textarea>, jejich vnitřní obsah se automaticky doplní. Značka <form n:name> navíc vytvoří lokální proměnnou $form s objektem kresleného formuláře a uzavírací </form> vykreslí všechny nevykreslené hidden prvky (totéž platí i pro {form} ... {/form}).

Nesmíme ovšem zapomenout na vykreslení možných chybových zpráv. A to jak těch, které se metodou addError() přidaly k jednotlivým prvkům (pomocí {inputError}), tak i těch přidaných přímo k formuláři (vrací je $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>

Složitější formulářové prvky, jako je RadioList nebo CheckboxList, lze takto vykreslovat po jednotlivých položkách:

{foreach $form[gender]->getItems() as $key => $label}
	<label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label>
{/foreach}

{label} {input}

Nechcete u každého prvku přemýšlet, jaký HTML element pro něj v šabloně použít, zda <input>, <textarea> atd? Řešením je univerzální značka {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>

Pokud formulář používá translator, bude text uvnitř značek {label} překládán.

I v tomto případě lze složitější formulářové prvky, jako je RadioList nebo CheckboxList, vykreslovat po jednotlivých položkách:

{foreach $form[gender]->items as $key => $label}
	{label gender:$key}{input gender:$key} {$label}{/label}
{/foreach}

Pro vykreslení samotného <input> v prvku Checkbox použijte {input myCheckbox:}. HTML atributy v tomto případě vždy oddělujte čárkou {input myCheckbox:, class: required}.

{inputError}

Vypíše chybovou zprávu k formulářovému prvku, pokud nějakou má. Zprávu obvykle zabalíme do HTML elementu kvůli stylování. Předejít vykreslování prázdného elementu, pokud zpráva není, lze elegantně pomocí n:ifcontent:

<span class=error n:ifcontent>{inputError $input}</span>

Přítomnost chyby můžeme zjistit metodou hasErrors() a podle toho nastavit třídu nadřazenému elementu:

<div n:class="$form[username]->hasErrors() ? 'error'">
	{input username}
	{inputError username}
</div>

{form}

Značky {form signInForm}...{/form} jsou alternativou k <form n:name="signInForm">...</form>.

Automatické vykreslování

Díky značkám {input} a {label} můžeme snadno vytvořit obecnou šablonu pro jakýkoliv formulář. Bude postupně iterovat a vykreslovat všechny jeho prvky, kromě hidden prvků, které se vykreslí automaticky při ukončení formuláře značkou </form>. Název vykreslovaného formuláře bude očekávat v proměnné $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>

Použité sebeuzavírající párové značky {label .../} zobrazují popisky pocházející z definice formuláře v PHP kódu.

Tuto obecnou šablonu si uložte třeba do souboru basic-form.latte a pro vykreslení formuláře ji stačí inkludovat a předat název (či instanci) formuláře do parametru $form:

{include basic-form.latte, form: signInForm}

Pokud byste při vykreslování jednoho určitého formuláře chtěli do jeho podoby zasáhnout a třeba jeden prvek vykreslit jinak, pak je nejjednodušší cestou si v šabloně předpřipravit bloky, které bude možné následně přepsat. Bloky mohou mít také dynamické názvy, lze do nich tak vložit i jméno vykreslovaného prvku. Například:

...
	{label $input /}
	{block "input-{$input->name}"}{input $input}{/block}
...

Pro prvek např. username tak vznikne blok input-username, který lze snadno přepsat použitím značky {embed}:

{embed basic-form.latte, form: signInForm}
	{block input-username}
		<span class=important>
			{include parent}
		</span>
	{/block}
{/embed}

Alternativně lze celý obsah šablony basic-form.latte definovat jako blok, včetně parametru $form:

{define basic-form, $form}
	<form n:name=$form class=form>
		...
	</form>
{/define}

Díky tomu bude mírně jednodušší jeho volání:

{embed basic-form, signInForm}
	...
{/embed}

Blok přitom stačí importovat na jediném místě a to na začátku šablony layoutu:

{import basic-form.latte}

Speciální případy

Pokud potřebujete vykreslit jen vnitřní část formuláře bez HTML značek <form>, například při posílání snippetů, skryjte je pomocí atributu 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>

S vykreslením prvků uvnitř formulářového kontejneru pomůže tag {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}

Vykreslení bez Latte

Nejjednodušší způsob, jak vykreslit formulář, je zavolat:

$form->render();

Ovlivnit podobu takto vykresleného formuláře lze konfigurací Rendereru a jednotlivých prvků.

Manuální vykreslení

Každý formulářový prvek disponuje metodami, které generují HTML kód formulářového políčka a popisky. Mohou jej vracet buď jako řetězec nebo objekt Nette\Utils\Html:

  • getControl(): Html|string vrací HTML kód prvku
  • getLabel($caption = null): Html|string|null vrací HTML kód popisky, pokud existuje

Formulář tak lze vykreslovat po jednotlivých elementech:

<?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') ?>

Zatímco u některých prvků vrací getControl() jediný HTML element (např. <input>, <select> apod.), u jiných celý kus HTML kódu (CheckboxList, RadioList). V takovém případě můžete využít metod, které generují jednotlivé inputy a popisky, pro každou položku zvlášt:

  • getControlPart($key = null): ?Html vrací HTML kód jedné položky
  • getLabelPart($key = null): ?Html vrací HTML kód popisky jedené položky

Tyto metody mají z historických důvodů prefix get, ale lepší by byl generate, protože při každém volání vytvoří a vrátí nový element Html.

Renderer

Jde o objekt zajišťující vykreslení formuláře. Ten lze nastavit metodou $form->setRenderer. Předá se mu řízení při zavolání metody $form->render().

Pokud nenastavíme vlastní renderer, bude použit výchozí vykreslovač Nette\Forms\Rendering\DefaultFormRenderer. Ten prvky formuláře vykreslí do podoby HTML tabulky. Výstup vypadá takto:

<table>
<tr class="required">
	<th><label class="required" for="frm-name">Jméno:</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">Věk:</label></th>

	<td><input type="text" class="text" name="age" id="frm-age" required value=""></td>
</tr>

<tr>
	<th><label>Pohlaví:</label></th>
	...

Zda použít nebo nepoužít pro kostru formuláře tabulku je sporné a řada webdesignerů preferuje jiný markup. Například definiční seznam. Překonfigurujeme proto DefaultFormRenderer tak, aby formulář v podobě seznamu vykreslil. Konfigurace se provádí editací pole $wrappers. První index vždy představuje oblast a druhý její atribut. Jednotlivé oblasti znázorňuje obrázek:

Standardně je skupina prvků controls obalena tabulkou <table>, každý pair představuje řádek tabulky <tr> a dvojice label a control jsou buňky <th> a <td>. Nyní obalující elementy změníme. Oblast controls vložíme do kontejneru <dl>, oblast pair necháme bez kontejneru, label vložíme do <dt> a nakonec control obalíme značkami <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();

Výsledkem je tento HTML kód:

<dl>
	<dt><label class="required" for="frm-name">Jméno:</label></dt>

	<dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd>


	<dt><label class="required" for="frm-age">Věk:</label></dt>

	<dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd>


	<dt><label>Pohlaví:</label></dt>
	...
</dl>

V poli wrappers lze ovlivnit celou řadu dalších atributů:

  • přidávat CSS třídy jednotlivým typům formulářových prvků
  • rozlišovat CSS třídou liché a sudé řádky
  • vizuálně odlišit povinné a volitelné položky
  • určovat, zda se chybové zprávy zobrazí přímo u prvků nebo nad formulářem

Options

Chování Rendereru lze ovládat i nastavováním options na jednotlivých formulářových prvcích. Takto lze nastavit popisek, který se vypíše vedle vstupního pole:

$form->addText('phone', 'Číslo:')
	->setOption('description', 'Toto číslo zůstane skryté');

Pokud do něj chceme umístit HTML obsah, využijeme třídy Html

use Nette\Utils\Html;

$form->addText('phone', 'Číslo:')
	->setOption('description', Html::el('p')
		->setHtml('<a href="...">Podmínky uchovávání Vašeho čísla</a>')
	);

Html prvek lze využít i místo labelu: $form->addCheckbox('conditions', $label).

Seskupování prvků

Renderer umožňuje seskupovat prvky do vizuálních skupin (fieldsetů):

$form->addGroup('Personal data');

Po vytvoření nové skupiny se tato stává aktivní a každý nově přidaný prvek je zároveň přidán i do ní. Takže formulář lze stavět tímto způsobem:

$form = new Form;
$form->addGroup('Personal data');
$form->addText('name', 'Your name:');
$form->addInteger('age', 'Your age:');
$form->addEmail('email', 'Email:');

$form->addGroup('Shipping address');
$form->addCheckbox('send', 'Ship to address');
$form->addText('street', 'Street:');
$form->addText('city', 'City:');
$form->addSelect('country', 'Country:', $countries);

Renderer nejprve vykresluje skupiny a teprve poté prvky, které do žádné skupiny nepatří.

Podpora pro Bootstrap

V příkladech najdete ukázky, jak nakonfigurovat Renderer pro Twitter Bootstrap 2, Bootstrap 3 a Bootstrap 4

HTML atributy

Formulářovým prvkům můžeme nastavovat libovolné HTML atributy pomocí setHtmlAttribute(string $name, $value = true):

$form->addInteger('number', 'Číslo:')
	->setHtmlAttribute('class', 'big-number');

$form->addSelect('rank', 'Řazení dle:', ['ceny', 'názvu'])
	->setHtmlAttribute('onchange', 'submit()'); // při změně odeslat


// chceme-li to samé udělat pro <form>
$form->setHtmlAttribute('id', 'myForm');

Nastavení typu:

$form->addText('tel', 'Váš telefon:')
	->setHtmlType('tel')
	->setHtmlAttribute('placeholder', 'napište telefon');

Jednotlivým položkám v radio nebo checkbox listech můžeme nastavit HTML atribut s rozdílnými hodnotami pro každou z nich. Povšimněte si dvojtečky za style:, která zajistí volbu hodnoty podle klíče:

$colors = ['r' => 'červená', 'g' => 'zelená', 'b' => 'modrá'];
$styles = ['r' => 'background:red', 'g' => 'background:green'];
$form->addCheckboxList('colors', 'Barvy:', $colors)
	->setHtmlAttribute('style:', $styles);

Vypíše:

<label><input type="checkbox" name="colors[]" style="background:red" value="r">červená</label>
<label><input type="checkbox" name="colors[]" style="background:green" value="g">zelená</label>
<label><input type="checkbox" name="colors[]" value="b">modrá</label>

Pokud jde o logický HTML atribut (který nemá hodnotu, např. readonly), můžeme použít zápis s otazníkem:

$colors = ['r' => 'červená', 'g' => 'zelená', 'b' => 'modrá'];
$form->addCheckboxList('colors', 'Barvy:', $colors)
	->setHtmlAttribute('readonly?', 'r'); // pro více klíču použijte pole, např. ['r', 'g']

Vypíše:

<label><input type="checkbox" name="colors[]" readonly value="r">červená</label>
<label><input type="checkbox" name="colors[]" value="g">zelená</label>
<label><input type="checkbox" name="colors[]" value="b">modrá</label>

V případě selectboxů metoda setHtmlAttribute() nastavuje atributy elementu <select>. Pokud chceme nastavit atributy jednotlivým <option>, použijeme metodu setOptionAttribute(). Fungují i zápisy s dvojtečkou a otazníkem uvedené výše:

$form->addSelect('colors', 'Barvy:', $colors)
	->setOptionAttribute('style:', $styles);

Vypíše:

<select name="colors">
	<option value="r" style="background:red">červená</option>
	<option value="g" style="background:green">zelená</option>
	<option value="b">modrá</option>
</select>

Prototypy

Alternativní způsob nastavování HTML atributů spočívá v úpravě předlohy, ze které se HTML element generuje. Předlohou je objekt Html a vrací jej metoda getControlPrototype():

$input = $form->addInteger('number', 'Číslo:');
$html = $input->getControlPrototype(); // <input>
$html->class('big-number');            // <input class="big-number">

Tímto způsobem lze modifikovat i předlohu popisky, kterou vrací getLabelPrototype():

$html = $input->getLabelPrototype(); // <label>
$html->class('distinctive');         // <label class="distinctive">

U prvků Checkbox, CheckboxList a RadioList můžete ovlivnit předlohu elementu, který celý prvek obaluje. Vrací jej getContainerPrototype(). Ve výchozím stavu jde o „prázdný“ element, takže se nic nevykresluje, ale tím, že mu nastavíme název, se vykreslovat bude:

$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>

V připadě CheckboxList a RadioList lze ovlivnit i předlohu oddělovače jednotlivých položek, který vrací metoda getSeparatorPrototype(). Ve výchozím stavu je to element <br>. Pokud jej změníte na párový element, bude jednotlivé položky obalovat místo oddělovat. A dále lze ovlivnit předlohu HTML elementu popisky u jednotlivých položek, který vrací getItemLabelPrototype().

Překládání

Pokud programujete vícejazyčnou aplikaci, budete nejspíš potřebovat formulář vykreslit v různých jazykových mutacích. Nette Framework k tomuto účelu definuje rozhraní pro překlad Nette\Localization\Translator. V Nette není žádná výchozí implementace, můžete si vybrat podle svých potřeb z několika hotových řešeních, které najdete na Componette. V jejich dokumentaci se dozvíte, jak translator konfigurovat.

Formuláře podporují vypisování textů přes translator. Předáme jim ho pomocí metody setTranslator():

$form->setTranslator($translator);

Od této chvíle se nejen všechny popisky, ale i všechny chybové hlášky nebo položky select boxů přeloží do jiného jazyka.

U jednotlivých formulářových prvků je přitom možné nastavit jiný překladač nebo překládání úplně vypnout hodnotou null:

$form->addSelect('carModel', 'Model:', $cars)
	->setTranslator(null);

validačních pravidel se translatoru předávají i specifické parametry, například u pravidla:

$form->addPassword('password', 'Heslo:')
	->addCondition($form::MinLength, 'Heslo musí mít alespoň %d znaků', 8);

se volá translator s těmito parametry:

$translator->translate('Heslo musí mít alespoň %d znaků', 8);

a tedy může zvolit správný tvar plurálu u slova znaky podle počtu.

Událost onRender

Těsně před tím, než se formulář vykreslí, můžeme nechat zavolat náš kód. Ten může například doplnit formulářovým prvkům HTML třídy pro správné zobrazení. Kód přidáme do pole onRender:

$form->onRender[] = function ($form) {
	BootstrapCSS::initialize($form);
};
verze: 4.0 3.x 2.x