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 prvkugetLabel($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žkygetLabelPart($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
Pro nastavení libovolných HTML atributů formulářových prvků použijeme metodu
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
// Pro nastavení atributů samotného <form>
$form->setHtmlAttribute('id', 'myForm');
Specifikace typu prvku:
$form->addText('tel', 'Váš telefon:')
->setHtmlType('tel')
->setHtmlAttribute('placeholder', 'napište telefon');
Nastavení typu a dalších atributů slouží jen pro vizuální účely. Ověření správnosti vstupů musí probíhat na serveru, což zajístíte volbou vhodného formulářového prvku a uvedením validačních pravidel.
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>
Pro nastavení logických atributů, jako je readonly
, můžeme použít zápis s otazníkem:
$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');
$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);
U validačních pravidel se translatoru předávají i specifické parametry, například u pravidla:
$form->addPassword('password', 'Heslo:')
->addRule($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);
};