Renderowanie formularzy
Wygląd formularzy może być bardzo różnorodny. W praktyce możemy napotkać dwie skrajności. Z jednej strony jest
potrzeba wyrenderowania w aplikacji wielu formularzy, które wyglądają identycznie, i doceniamy łatwe renderowanie bez
szablonu za pomocą $form->render(). Zwykle jest tak w interfejsach administracyjnych.
Z drugiej strony są różnorodne formularze, z których każdy jest wyjątkowy. Ich wygląd najlepiej opisać HTML-em w szablonie formularza. I oczywiście oprócz tych dwóch skrajności napotkamy mnóstwo formularzy leżących gdzieś pośrodku.
Renderowanie z Latte
System szablonów Latte zasadniczo upraszcza renderowanie formularzy i ich elementów. Najpierw pokażemy, jak renderować formularz ręcznie, element po elemencie, i uzyskać pełną kontrolę nad kodem. Później pokażemy, jak takie renderowanie zautomatyzować.
Propozycję szablonu Latte dla formularza możesz wygenerować metodą
Nette\Forms\Blueprint::latte($form), która wypisze go na stronie w przeglądarce. Następnie wystarczy kliknięciem
zaznaczyć kod i skopiować go do projektu.
{control}
Najprostszym sposobem wyrenderowania formularza jest napisanie w szablonie:
{control signInForm}
Na wygląd wyrenderowanego formularza można wpłynąć konfiguracją Renderer i poszczególnych elementów.
n:name
Powiązanie definicji formularza w kodzie PHP z kodem HTML jest niezwykle łatwe. Wystarczy dodać atrybuty
n:name. Tak prosto to działa!
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>Nazwa użytkownika: <input n:name=username size=20 autofocus></label>
</div>
<div>
<label n:name=password>Hasło: <input n:name=password></label>
</div>
<div>
<input n:name=send class="btn btn-default">
</div>
</form>
Masz pełną kontrolę nad wyglądem wynikowego kodu HTML. Jeśli użyjesz atrybutu n:name przy elementach
<select>, <button> albo <textarea>, ich wewnętrzna zawartość zostanie
uzupełniona automatycznie. Poza tym tag <form n:name> tworzy lokalną zmienną $form z obiektem
renderowanego formularza, a zamykający tag </form> renderuje wszystkie niewyrenderowane elementy ukryte (to
samo dotyczy {form} ... {/form}).
Nie możemy jednak zapomnieć o wyrenderowaniu ewentualnych komunikatów o błędach. Chodzi zarówno o te dodane do
poszczególnych elementów metodą addError() (renderowane przez {inputError}), jak i o te dodane
bezpośrednio do formularza (zwracane przez $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>Nazwa użytkownika: <input n:name=username size=20 autofocus></label>
<span class=error n:ifcontent>{inputError username}</span>
</div>
<div>
<label n:name=password>Hasło: <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>
Bardziej złożone elementy formularza, jak RadioList albo CheckboxList, można renderować pozycja po pozycji tak:
{foreach $form[gender]->getItems() as $key => $label}
<label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label>
{/foreach}
{label} {input}
Wolisz nie zastanawiać się w szablonie, jakiego elementu HTML użyć dla danego elementu formularza, czy
<input>, czy <textarea> itd.? Rozwiązaniem jest uniwersalny tag {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}Nazwa użytkownika: {input username, size: 20, autofocus: true}{/label}
{inputError username}
</div>
<div>
{label password}Hasło: {input password}{/label}
{inputError password}
</div>
<div>
{input send, class: "btn btn-default"}
</div>
</form>
Jeśli formularz używa translatora, etykiety renderowane z definicji formularza (np. {label username /}) są
tłumaczone. Tekst zapisany bezpośrednio między tagami {label} i {/label} już nie.
Znów bardziej złożone elementy formularza, jak RadioList albo CheckboxList, można renderować pozycja po pozycji:
{foreach $form[gender]->items as $key => $label}
{label gender:$key}{input gender:$key} {$label}{/label}
{/foreach}
Żeby wyrenderować sam <input> dla elementu Checkbox, użyj {input myCheckbox:}. W takim
przypadku zawsze oddzielaj atrybuty HTML przecinkiem: {input myCheckbox:, class: required}.
{inputError}
Wypisuje komunikat o błędzie elementu formularza, jeśli taki istnieje. Komunikat zwykle opakowujemy w element HTML do
ostylowania. Zapobiec renderowaniu pustego elementu, gdy komunikatu nie ma, można elegancko za pomocą
n:ifcontent:
<span class=error n:ifcontent>{inputError $input}</span>
Obecność błędu możemy sprawdzić metodą hasErrors() i odpowiednio ustawić klasę elementu
nadrzędnego:
<div n:class="$form[username]->hasErrors() ? 'error'">
{input username}
{inputError username}
</div>
{form}
Tagi {form signInForm}...{/form} są alternatywą dla
<form n:name="signInForm">...</form>. Ewentualne argumenty oddziel od nazwy przecinkiem:
{form signInForm, class: foo}.
Słowo kluczowe scope umieszczone przed nazwą tylko odkłada formularz na stos (żeby
{input}, {label} itd. się z nim wiązały), ale nie renderuje tagu <form>. Przydaje
się do renderowania części formularza, np. w snippecie. Jeśli jakiś formularz jest już aktywny, nazwa rozwiązywana jest
względem niego, więc {form scope} zastępuje też {formContainer}:
{form scope signInForm}
{input username}
{/form}
Słowo kluczowe detached renderuje pusty <form></form> i wiąże
z nim każdy element przez atrybut HTML form. Pozwala to umieścić formularz wewnątrz innego formularza, czego
HTML normalnie zabrania. Odłączony formularz musi mieć HTML-owe id, które generowane jest automatycznie, gdy
nadasz mu nazwę (jak outerForm poniżej):
{form detached outerForm}
...
{/form}
Renderowanie automatyczne
Dzięki tagom {input} i {label} możemy łatwo utworzyć ogólny szablon dla dowolnego formularza.
Będzie przechodzić przez wszystkie jego elementy i je renderować, z wyjątkiem elementów ukrytych, które renderowane są
automatycznie przy zamknięciu formularza tagiem </form>. Oczekuje nazwy renderowanego formularza w zmiennej
$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>
Użyte tu samozamykające się tagi parzyste {label .../} wypisują etykiety pochodzące z definicji formularza w
kodzie PHP.
Ten ogólny szablon zapisz na przykład w pliku basic-form.latte. Żeby wyrenderować formularz, wystarczy go
dołączyć i przekazać nazwę formularza (albo instancję) do parametru $form:
{include basic-form.latte, form: signInForm}
Jeśli chcesz przy renderowaniu zmodyfikować wygląd konkretnego formularza, na przykład wyrenderować jeden element inaczej, najprościej jest przygotować w szablonie bloki, które można następnie nadpisać. Bloki mogą mieć też dynamiczne nazwy, dzięki czemu możesz wstawić do nich nazwę renderowanego elementu. Na przykład:
...
{label $input /}
{block "input-{$input->name}"}{input $input}{/block}
...
Dla elementu o nazwie np. username powstanie blok input-username, który można łatwo nadpisać
tagiem {embed}:
{embed basic-form.latte, form: signInForm}
{block input-username}
<span class=important>
{include parent}
</span>
{/block}
{/embed}
Alternatywnie całą zawartość szablonu basic-form.latte można zdefiniować jako blok, włącznie z parametrem
$form:
{define basic-form, $form}
<form n:name=$form class=form>
...
</form>
{/define}
Dzięki temu jego wywołanie będzie odrobinę prostsze:
{embed basic-form, signInForm}
...
{/embed}
Blok wystarczy zaimportować w jednym miejscu, na początku szablonu layoutu:
{import basic-form.latte}
Przypadki szczególne
Jeśli potrzebujesz wyrenderować tylko wewnętrzną część formularza bez tagów HTML <form>, na
przykład przy wysyłaniu snippetów, ukryj je atrybutem n:tag-if:
<form n:name=signInForm n:tag-if=false>
<div>
<label n:name=username>Nazwa użytkownika: <input n:name=username></label>
{inputError username}
</div>
</form>
Z renderowaniem elementów wewnątrz kontenera formularza pomaga tag {formContainer}, względnie nowszy {form scope}.
<p>Jakie wiadomości chcesz otrzymywać:</p>
{formContainer emailNews}
<ul>
<li>{input sport} {label sport /}</li>
<li>{input science} {label science /}</li>
</ul>
{/formContainer}
Renderowanie bez Latte
Najprostszym sposobem wyrenderowania formularza jest wywołanie:
$form->render();
Na wygląd wyrenderowanego formularza można wpłynąć konfiguracją Renderer i poszczególnych elementów.
Renderowanie ręczne
Każdy element formularza ma metody generujące kod HTML pola formularza i jego etykiety. Mogą go zwracać albo jako ciąg, albo jako obiekt Nette\Utils\Html:
getControl(): Html|stringzwraca kod HTML elementugetLabel($caption = null): Html|string|nullzwraca kod HTML etykiety, jeśli istnieje
Pozwala to renderować formularz element po elemencie:
<?php $form->render('begin') ?>
<?php $form->render('ownerrors') ?>
<div>
<?= $form['name']->getLabel() ?>
<?= $form['name']->getControl() ?>
<span class=error><?= htmlspecialchars((string) $form['name']->getError()) ?></span>
</div>
<div>
<?= $form['age']->getLabel() ?>
<?= $form['age']->getControl() ?>
<span class=error><?= htmlspecialchars((string) $form['age']->getError()) ?></span>
</div>
// ...
<?php $form->render('end') ?>
Podczas gdy dla niektórych elementów getControl() zwraca pojedynczy element HTML (np.
<input>, <select> itd.), dla innych zwraca cały kawałek kodu HTML (CheckboxList,
RadioList). W takich przypadkach możesz użyć metod generujących osobno poszczególne inputy i etykiety dla każdej
pozycji:
getControlPart($key = null): Htmlzwraca kod HTML jednej pozycjigetLabelPart($key = null): Htmlzwraca kod HTML etykiety jednej pozycji
Metody te mają ze względów historycznych przedrostek get, ale bardziej odpowiedni byłby
generate, bo przy każdym wywołaniu tworzą i zwracają nowy element Html.
Renderer
To obiekt zapewniający wyrenderowanie formularza. Można go ustawić metodą $form->setRenderer(). Kontrola
przekazywana jest mu w momencie wywołania metody $form->render().
Jeśli nie ustawimy własnego renderera, użyty zostanie domyślny renderer Nette\Forms\Rendering\DefaultFormRenderer. Ten renderuje elementy formularza do tabeli HTML. Wynik wygląda tak:
<table>
<tr class="required">
<th><label class="required" for="frm-name">Imię:</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">Wiek:</label></th>
<td><input type="text" class="text" name="age" id="frm-age" required value=""></td>
</tr>
<tr>
<th><label>Płeć:</label></th>
...
To, czy używać tabeli do struktury formularza, jest dyskusyjne, a wielu webdesignerów woli inny markup, na przykład listę
definicyjną. Przekonfigurujemy więc DefaultFormRenderer tak, żeby renderował formularz jako listę. Konfiguracja
odbywa się przez edycję tablicy $wrappers. Pierwszy
indeks zawsze reprezentuje obszar, a drugi jego atrybut. Poszczególne obszary pokazuje rysunek:

Domyślnie grupa controls opakowana jest w <table>, każdy pair reprezentuje
wiersz tabeli <tr>, a para label i control to komórki <th> i
<td>. Teraz zmienimy elementy opakowujące. Obszar controls umieścimy w kontenerze
<dl>, obszar pair zostawimy bez kontenera, label wstawimy do <dt>,
a na koniec control opakujemy tagami <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();
Powstanie w ten sposób poniższy kod HTML:
<dl>
<dt><label class="required" for="frm-name">Imię:</label></dt>
<dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd>
<dt><label class="required" for="frm-age">Wiek:</label></dt>
<dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd>
<dt><label>Płeć:</label></dt>
...
</dl>
Tablica wrappers pozwala wpływać na wiele innych atrybutów:
- dodawanie klas CSS poszczególnym typom elementów formularza
- rozróżnianie klasami CSS wierszy nieparzystych i parzystych
- wizualne rozróżnianie pozycji obowiązkowych i opcjonalnych
- określanie, czy komunikaty o błędach wyświetlają się bezpośrednio przy elementach, czy nad formularzem
Opcje
Zachowanie Renderera można sterować także ustawianiem opcji poszczególnym elementom formularza. W ten sposób ustawisz opis, który wyświetli się obok pola:
$form->addText('phone', 'Numer:')
->setOption('description', 'Ten numer pozostanie ukryty');
Jeśli chcemy umieścić w nim treść HTML, użyjemy klasy Html:
use Nette\Utils\Html;
$form->addText('phone', 'Telefon:')
->setOption('description', Html::el('p')
->setHtml('<a href="...">Regulamin usługi.</a>')
);
Elementu Html można użyć także zamiast etykiety: $form->addCheckbox('conditions', $label).
Grupowanie elementów
Renderer pozwala grupować elementy w wizualne grupy (fieldsety):
$form->addGroup('Dane osobowe');
Po utworzeniu nowej grupy staje się ona aktywna i każdy nowo dodany element jest dodawany także do niej. Formularz można więc budować w ten sposób:
$form = new Form;
$form->addGroup('Dane osobowe');
$form->addText('name', 'Twoje imię:');
$form->addInteger('age', 'Twój wiek:');
$form->addEmail('email', 'Email:');
$form->addGroup('Adres dostawy');
$form->addCheckbox('send', 'Wyślij na adres');
$form->addText('street', 'Ulica:');
$form->addText('city', 'Miasto:');
$form->addSelect('country', 'Kraj:', $countries);
Renderer rysuje najpierw grupy, a dopiero potem elementy, które nie należą do żadnej grupy.
Wsparcie dla Bootstrapa
W katalogu examples znajdziesz przykłady pokazujące, jak skonfigurować Renderer dla Twitter Bootstrap 2, Bootstrap 3 i Bootstrap 4.
Atrybuty HTML
Do ustawiania dowolnych atrybutów HTML elementom formularza służy metoda
setHtmlAttribute(string $name, $value = true):
$form->addInteger('number', 'Liczba:')
->setHtmlAttribute('class', 'big-number');
$form->addSelect('rank', 'Sortuj według:', ['cena', 'nazwa'])
->setHtmlAttribute('onchange', 'submit()'); // wysyła formularz przy zmianie
// żeby ustawić atrybuty samego elementu <form>
$form->setHtmlAttribute('id', 'myForm');
Podanie typu elementu:
$form->addText('tel', 'Twój telefon:')
->setHtmlType('tel')
->setHtmlAttribute('placeholder', 'Podaj swój telefon');
Ustawianie typu i innych atrybutów służy tylko celom wizualnym. Weryfikacja poprawności wejścia musi odbywać się po stronie serwera, co zapewnisz wyborem odpowiedniego elementu formularza i podaniem reguł walidacyjnych.
Poszczególnym pozycjom w listach radio i checkbox możemy ustawić atrybut HTML o różnych wartościach dla każdej
z nich. Zwróć uwagę na dwukropek po style:, który zapewnia wybór wartości według klucza:
$colors = ['r' => 'czerwony', 'g' => 'zielony', 'b' => 'niebieski'];
$styles = ['r' => 'background:red', 'g' => 'background:green'];
$form->addCheckboxList('colors', 'Kolory:', $colors)
->setHtmlAttribute('style:', $styles);
Wyrenderuje:
<label><input type="checkbox" name="colors[]" style="background:red" value="r">czerwony</label>
<label><input type="checkbox" name="colors[]" style="background:green" value="g">zielony</label>
<label><input type="checkbox" name="colors[]" value="b">niebieski</label>
Do ustawiania atrybutów logicznych, jak readonly, możemy użyć zapisu ze znakiem zapytania:
$form->addCheckboxList('colors', 'Kolory:', $colors)
->setHtmlAttribute('readonly?', 'r'); // dla wielu kluczy użyj tablicy, np. ['r', 'g']
Wyrenderuje:
<label><input type="checkbox" name="colors[]" readonly value="r">czerwony</label>
<label><input type="checkbox" name="colors[]" value="g">zielony</label>
<label><input type="checkbox" name="colors[]" value="b">niebieski</label>
Przy selectboxach metoda setHtmlAttribute() ustawia atrybuty elementu <select>. Jeśli chcemy
ustawić atrybuty poszczególnym elementom <option>, użyjemy metody setOptionAttribute().
Wspomniane wyżej zapisy z dwukropkiem i znakiem zapytania również działają:
$form->addSelect('colors', 'Kolory:', $colors)
->setOptionAttribute('style:', $styles);
Wyrenderuje:
<select name="colors">
<option value="r" style="background:red">czerwony</option>
<option value="g" style="background:green">zielony</option>
<option value="b">niebieski</option>
</select>
Prototypy
Alternatywnym sposobem ustawiania atrybutów HTML jest modyfikacja szablonu, z którego generowany jest element HTML. Szablon
jest obiektem Html i zwraca go metoda getControlPrototype():
$input = $form->addInteger('number', 'Liczba:');
$html = $input->getControlPrototype(); // <input>
$html->class('big-number'); // <input class="big-number">
W ten sposób można modyfikować także szablon etykiety zwracany przez getLabelPrototype():
$html = $input->getLabelPrototype(); // <label>
$html->class('distinctive'); // <label class="distinctive">
Przy elementach Checkbox, CheckboxList i RadioList możesz wpłynąć na szablon elementu, który opakowuje cały element
formularza. Zwraca go getContainerPrototype(). Domyślnie jest to “pusty” element, więc nic się nie renderuje,
ale gdy nadasz mu nazwę, zostanie wyrenderowany:
$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>
W przypadku CheckboxList i RadioList możesz wpłynąć także na szablon separatora poszczególnych pozycji zwracany metodą
getSeparatorPrototype(). Domyślnie jest to element <br>. Jeśli zmienisz go na element parzysty,
będzie opakowywał poszczególne pozycje zamiast je oddzielać. Poza tym możesz wpłynąć na szablon elementu HTML dla etykiet
poszczególnych pozycji, zwracany przez getItemLabelPrototype().
Tłumaczenie
Jeśli tworzysz aplikację wielojęzyczną, prawdopodobnie będziesz potrzebować wyrenderować formularz w różnych wersjach językowych. Nette Framework definiuje w tym celu interfejs tłumaczący Nette\Localization\Translator. Nette nie ma domyślnej implementacji, możesz wybrać zgodnie ze swoimi potrzebami spośród kilku gotowych rozwiązań, które znajdziesz na Componette. Ich dokumentacja wyjaśnia, jak skonfigurować translator.
Formularze wspierają wypisywanie tekstów przez translator. Przekazujemy go metodą setTranslator():
$form->setTranslator($translator);
Od tego momentu na język docelowy tłumaczone będą nie tylko wszystkie etykiety, ale też wszystkie komunikaty o błędach, pozycje w selectboxach czy placeholdery.
Poszczególnym elementom formularza można ustawić inny translator albo tłumaczenie całkowicie wyłączyć, ustawiając
wartość null:
$form->addSelect('carModel', 'Model:', $cars)
->setTranslator(null);
Przy regułach walidacyjnych translatorowi przekazywane są także konkretne parametry. Na przykład dla reguły:
$form->addPassword('password', 'Hasło:')
->addRule($form::MinLength, 'Hasło musi mieć co najmniej %d znaków', 8);
translator wywoływany jest z tymi parametrami:
$translator->translate('Hasło musi mieć co najmniej %d znaków', 8);
i może więc wybrać poprawną formę liczby mnogiej słowa znaków zależnie od liczby.
Zdarzenie onRender
Tuż przed wyrenderowaniem formularza możemy wywołać własny kod. Ten może na przykład dodać elementom formularza klasy
HTML dla poprawnego wyświetlenia. Kod dodajemy do tablicy onRender:
$form->onRender[] = function ($form) {
BootstrapCSS::initialize($form);
};