Renderowanie formularzy
Wygląd formularzy może być bardzo różnorodny. W praktyce możemy napotkać dwa ekstrema. Z jednej strony stoi potrzeba
renderowania w aplikacji wielu formularzy, które są wizualnie podobne jak dwie krople wody, i docenimy łatwe renderowanie bez
szablonu za pomocą $form->render()
. Jest to zazwyczaj przypadek interfejsów administracyjnych.
Z drugiej strony mamy różnorodne formularze, gdzie obowiązuje zasada: co sztuka, to oryginał. Ich postać najlepiej opiszemy językiem HTML w szablonie formularza. I oczywiście oprócz obu wspomnianych ekstremów napotkamy wiele formularzy, które znajdują się gdzieś pomiędzy.
Renderowanie za pomocą Latte
System szablonów Latte znacznie ułatwia renderowanie formularzy i ich elementów. Najpierw pokażemy, jak renderować formularze ręcznie po poszczególnych elementach i tym samym uzyskać pełną kontrolę nad kodem. Później pokażemy, jak można takie renderowanie zautomatyzować.
Projekt szablonu Latte formularza możesz sobie wygenerować za pomocą metody
Nette\Forms\Blueprint::latte($form)
, która wypisze go na stronie przeglądarki. Kod następnie wystarczy
kliknięciem zaznaczyć i skopiować do projektu.
{control}
Najprostszym sposobem renderowania formularza jest napisanie w szablonie:
{control signInForm}
Można wpłynąć na wygląd tak renderowanego formularza konfigurując Renderer i poszczególne elementy.
n:name
Definicję formularza w kodzie PHP można niezwykle łatwo powiązać z kodem HTML. Wystarczy tylko uzupełnić atrybuty
n:name
. Takie to proste!
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>
Postać wynikowego kodu HTML masz w pełni w swoich rękach. Jeśli atrybut n:name
użyjesz w elementach
<select>
, <button>
lub <textarea>
, ich wewnętrzna zawartość zostanie
automatycznie uzupełniona. Znacznik <form n:name>
dodatkowo tworzy lokalną zmienną $form
z obiektem renderowanego formularza, a zamykający </form>
renderuje wszystkie niewyrenderowane elementy
ukryte (to samo dotyczy również {form} ... {/form}
).
Nie możemy jednak zapomnieć o renderowaniu możliwych komunikatów błędów. Zarówno tych, które metodą
addError()
zostały dodane do poszczególnych elementów (za pomocą {inputError}
), jak i tych dodanych
bezpośrednio do formularza (zwraca 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>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, takie jak RadioList lub CheckboxList, można w ten sposób renderować po poszczególnych pozycjach:
{foreach $form[gender]->getItems() as $key => $label}
<label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label>
{/foreach}
{label}
{input}
Nie chcesz przy każdym elemencie zastanawiać się, jaki element HTML dla niego użyć w szablonie, czy
<input>
, <textarea>
itp? Rozwiązaniem jest uniwersalny znacznik {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, tekst wewnątrz znaczników {label}
będzie tłumaczony.
Również w tym przypadku bardziej złożone elementy formularza, takie jak RadioList lub CheckboxList, można renderować po poszczególnych pozycjach:
{foreach $form[gender]->items as $key => $label}
{label gender:$key}{input gender:$key} {$label}{/label}
{/foreach}
Do renderowania samego <input>
w elemencie Checkbox użyj {input myCheckbox:}
. Atrybuty HTML w
tym przypadku zawsze oddzielaj przecinkiem {input myCheckbox:, class: required}
.
{inputError}
Wypisuje komunikat błędu dla elementu formularza, jeśli jakiś ma. Komunikat zazwyczaj opakowujemy w element HTML w celu
stylizacji. Zapobiec renderowaniu pustego elementu, jeśli 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 według tego ustawić klasę nadrzędnemu
elementowi:
<div n:class="$form[username]->hasErrors() ? 'error'">
{input username}
{inputError username}
</div>
{form}
Znaczniki {form signInForm}...{/form}
są alternatywą dla
<form n:name="signInForm">...</form>
.
Automatyczne renderowanie
Dzięki znacznikom {input}
i {label}
możemy łatwo stworzyć ogólny szablon dla dowolnego
formularza. Będzie on stopniowo iterował i renderował wszystkie jego elementy, oprócz elementów ukrytych, które renderują
się automatycznie przy zakończeniu formularza znacznikiem </form>
. Nazwę renderowanego formularza będzie
oczekiwał 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 samozamykające się znaczniki parzyste {label .../}
wyświetlają etykiety pochodzące z definicji
formularza w kodzie PHP.
Ten ogólny szablon zapisz sobie na przykład do pliku basic-form.latte
, a do renderowania formularza wystarczy go
dołączyć i przekazać nazwę (lub instancję) formularza do parametru $form
:
{include basic-form.latte, form: signInForm}
Gdybyś przy renderowaniu jednego określonego formularza chciał wpłynąć na jego postać i na przykład jeden element wyrenderować inaczej, najprostszą drogą jest przygotowanie sobie w szablonie bloków, które będzie można następnie nadpisać. Bloki mogą mieć również nazwy dynamiczne, można w nie wstawić również nazwę renderowanego elementu. Na przykład:
...
{label $input /}
{block "input-{$input->name}"}{input $input}{/block}
...
Dla elementu np. username
powstanie blok input-username
, który można łatwo nadpisać użyciem
znacznika {embed}:
{embed basic-form.latte, form: signInForm}
{block input-username}
<span class=important>
{include parent}
</span>
{/block}
{/embed}
Alternatywnie można całą zawartość szablonu basic-form.latte
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 nieco prostsze:
{embed basic-form, signInForm}
...
{/embed}
Blok przy tym wystarczy zaimportować w jednym miejscu, na początku szablonu layoutu:
{import basic-form.latte}
Przypadki specjalne
Jeśli potrzebujesz wyrenderować tylko wewnętrzną część formularza bez znaczników HTML <form>
, na
przykład przy wysyłaniu snippetów, ukryj je za pomocą atrybutu 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 pomoże tag {formContainer}
.
<p>Które 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 renderowania formularza jest wywołanie:
$form->render();
Można wpłynąć na wygląd tak renderowanego formularza konfigurując Renderer i poszczególne elementy.
Ręczne renderowanie
Każdy element formularza dysponuje metodami, które generują kod HTML pola formularza i etykiety. Mogą go zwracać albo jako string, albo obiekt Nette\Utils\Html:
getControl(): Html|string
zwraca kod HTML elementugetLabel($caption = null): Html|string|null
zwraca kod HTML etykiety, jeśli istnieje
Formularz można więc renderować po poszczególnych elementach:
<?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') ?>
Podczas gdy u niektórych elementów getControl()
zwraca pojedynczy element HTML (np. <input>
,
<select>
itp.), u innych cały fragment kodu HTML (CheckboxList, RadioList). W takim przypadku możesz
wykorzystać metody, które generują poszczególne inputy i etykiety, dla każdej pozycji osobno:
getControlPart($key = null): ?Html
zwraca kod HTML jednej pozycjigetLabelPart($key = null): ?Html
zwraca kod HTML etykiety jednej pozycji
Te metody mają z historycznych powodów prefiks get
, ale lepszy byłby generate
,
ponieważ przy każdym wywołaniu tworzą i zwracają nowy element Html
.
Renderer
Jest to obiekt zapewniający renderowanie formularza. Można go ustawić metodą $form->setRenderer
. Przekazuje
mu się sterowanie przy wywołaniu metody $form->render()
.
Jeśli nie ustawimy własnego renderera, zostanie użyty domyślny renderer Nette\Forms\Rendering\DefaultFormRenderer. Ten renderuje elementy formularza w postaci tabeli HTML. Wyjście 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>
...
Czy używać, czy nie używać tabeli dla szkieletu formularza, jest kwestią sporną, a wielu webdesignerów preferuje inne
znaczniki. Na przykład listę definicji. Przekonfigurujemy więc DefaultFormRenderer
tak, aby formularz
wyrenderował w postaci listy. Konfiguracja odbywa się przez edycję tablicy $wrappers. Pierwszy
indeks zawsze reprezentuje obszar, a drugi jego atrybut. Poszczególne obszary ilustruje obrazek:

Standardowo grupa elementów controls
jest opakowana tabelą <table>
, każdy pair
reprezentuje wiersz tabeli <tr>
, a para label
i control
są komórkami
<th>
i <td>
. Teraz zmienimy elementy opakowujące. Obszar controls
włożymy do
kontenera <dl>
, obszar pair
zostawimy bez kontenera, label
włożymy do
<dt>
, a na końcu control
opakujemy znacznikami <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();
Wynikiem jest ten 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>
W tablicy wrappers można wpłynąć na wiele innych atrybutów:
- dodawać klasy CSS poszczególnym typom elementów formularza
- rozróżniać klasą CSS wiersze parzyste i nieparzyste
- wizualnie odróżniać pozycje obowiązkowe i opcjonalne
- określać, czy komunikaty błędów wyświetlą się bezpośrednio przy elementach, czy nad formularzem
Opcje
Zachowanie Renderera można kontrolować również ustawiając opcje na poszczególnych elementach formularza. W ten sposób można ustawić opis, który wypisze się obok pola wejściowego:
$form->addText('phone', 'Numer telefonu:')
->setOption('description', 'Ten numer pozostanie ukryty');
Jeśli chcemy w nim umieścić zawartość HTML, wykorzystamy klasę Html
use Nette\Utils\Html;
$form->addText('phone', 'Numer telefonu:')
->setOption('description', Html::el('p')
->setHtml('<a href="...">Warunki przechowywania Twojego numeru</a>')
);
Element Html można wykorzystać również zamiast etykiety:
$form->addCheckbox('conditions', $label)
.
Grupowanie elementów
Renderer umożliwia grupowanie elementów w wizualne grupy (fieldsety):
$form->addGroup('Dane osobowe');
Po utworzeniu nowej grupy staje się ona aktywna i każdy nowo dodany element jest jednocześnie dodawany również do niej. Więc formularz można 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 wysyłki');
$form->addCheckbox('send', 'Wyślij na adres');
$form->addText('street', 'Ulica:');
$form->addText('city', 'Miasto:');
$form->addSelect('country', 'Kraj:', $countries);
Renderer najpierw renderuje grupy, a dopiero potem elementy, które do żadnej grupy nie należą.
Wsparcie dla Bootstrap
W przykładach znajdziesz przykłady, jak skonfigurować Renderer dla Twitter Bootstrap 2, Bootstrap 3 i Bootstrap 4
Atrybuty HTML
Do ustawienia dowolnych atrybutów HTML elementów formularza użyjemy metody
setHtmlAttribute(string $name, $value = true)
:
$form->addInteger('number', 'Numer:')
->setHtmlAttribute('class', 'big-number');
$form->addSelect('rank', 'Sortuj wg:', ['ceny', 'nazwy'])
->setHtmlAttribute('onchange', 'submit()'); // wysłać przy zmianie
// Do ustawienia atrybutów samego <form>
$form->setHtmlAttribute('id', 'myForm');
Specyfikacja typu elementu:
$form->addText('tel', 'Twój telefon:')
->setHtmlType('tel')
->setHtmlAttribute('placeholder', 'wpisz numer telefonu');
Ustawienie typu i innych atrybutów służy tylko do celów wizualnych. Weryfikacja poprawności wejść musi odbywać się na serwerze, co zapewnisz wyborem odpowiedniego elementu formularza i podaniem reguł walidacyjnych.
Poszczególnym pozycjom w listach radio lub checkbox możemy ustawić atrybut HTML z różnymi wartościami dla każdej
z nich. Zwróć uwagę na dwukropek za 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);
Wypisze:
<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 ustawienia atrybutów logicznych, takich 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']
Wypisze:
<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>
W przypadku pól wyboru metoda setHtmlAttribute()
ustawia atrybuty elementu <select>
. Jeśli
chcemy ustawić atrybuty poszczególnym <option>
, użyjemy metody setOptionAttribute()
. Działają
również zapisy z dwukropkiem i znakiem zapytania podane wyżej:
$form->addSelect('colors', 'Kolory:', $colors)
->setOptionAttribute('style:', $styles);
Wypisze:
<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
Alternatywny sposób ustawiania atrybutów HTML polega na modyfikacji wzorca, z którego generowany jest element HTML. Wzorcem
jest obiekt Html
i zwraca go metoda getControlPrototype()
:
$input = $form->addInteger('number', 'Numer:');
$html = $input->getControlPrototype(); // <input>
$html->class('big-number'); // <input class="big-number">
W ten sposób można modyfikować również wzorzec etykiety, który zwraca getLabelPrototype()
:
$html = $input->getLabelPrototype(); // <label>
$html->class('distinctive'); // <label class="distinctive">
U elementów Checkbox, CheckboxList i RadioList możesz wpłynąć na wzorzec elementu, który cały element opakowuje.
Zwraca go getContainerPrototype()
. W stanie domyślnym jest to „pusty” element, więc nic się nie renderuje, ale
przez ustawienie mu nazwy, będzie się renderować:
$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żna wpłynąć również na wzorzec separatora poszczególnych pozycji, który zwraca
metoda getSeparatorPrototype()
. W stanie domyślnym jest to element <br>
. Jeśli zmienisz go na
element parzysty, będzie poszczególne pozycje opakowywał zamiast oddzielać. A dalej można wpłynąć na wzorzec elementu
HTML etykiety u poszczególnych pozycji, który zwraca getItemLabelPrototype()
.
Tłumaczenie
Jeśli programujesz aplikację wielojęzyczną, prawdopodobnie będziesz potrzebować wyrenderować formularz w różnych wersjach językowych. Nette Framework w tym celu definiuje interfejs do tłumaczenia Nette\Localization\Translator. W Nette nie ma żadnej domyślnej implementacji, możesz wybrać według swoich potrzeb z kilku gotowych rozwiązań, które znajdziesz na Componette. W ich dokumentacji dowiesz się, jak konfigurować translator.
Formularze obsługują wypisywanie tekstów przez translator. Przekażemy im go za pomocą metody
setTranslator()
:
$form->setTranslator($translator);
Od tej chwili nie tylko wszystkie etykiety, ale i wszystkie komunikaty błędów lub pozycje pól wyboru zostaną przetłumaczone na inny język.
U poszczególnych elementów formularza można przy tym ustawić inny translator lub tłumaczenie całkowicie wyłączyć
wartością null
:
$form->addSelect('carModel', 'Model:', $cars)
->setTranslator(null);
U reguł walidacyjnych translatorowi przekazywane są również specyficzne parametry, na przykład u reguły:
$form->addPassword('password', 'Hasło:')
->addRule($form::MinLength, 'Hasło musi mieć co najmniej %d znaków', 8);
wywoływany jest translator z tymi parametrami:
$translator->translate('Hasło musi mieć co najmniej %d znaków', 8);
a więc może wybrać poprawną formę liczby mnogiej u słowa znaków
według liczby.
Zdarzenie onRender
Tuż przed tym, jak formularz zostanie wyrenderowany, możemy pozwolić wywołać nasz kod. Ten może na przykład uzupełnić
elementom formularza klasy HTML dla poprawnego wyświetlenia. Kod dodamy do tablicy onRender
:
$form->onRender[] = function ($form) {
BootstrapCSS::initialize($form);
};