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í
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. A samozřejmě kromě obou zmíněných extrémů narazíme na spoustu formulářů, které se pohybují někde mezi.
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.
{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}
Návrh kódu {formPrint}
Podobný Latte kód pro formulář si můžete nechat vygenerovat pomocí značky {formPrint}
. Pokud ji umístíte
do šablony, místo běžného vykreslení se zobrazí návrh kódu. Ten pak stačí označit a zkopírovat do projektu.
{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>
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}
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" 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" 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" value=""></dd>
<dt><label class="required" for="frm-age">Věk:</label></dt>
<dd><input type="text" class="text" name="age" id="frm-age" 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()
.
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);
};