Rendering formularzy
Wygląd form może być bardzo zróżnicowany. W praktyce możemy spotkać się z dwoma skrajnościami. Z jednej strony
istnieje potrzeba renderowania w aplikacji serii formularzy, które są do siebie wizualnie podobne, a my cenimy sobie łatwe
renderowanie bez szablonu za pomocą $form->render()
. Taka sytuacja ma miejsce zazwyczaj w przypadku interfejsów
administracyjnych.
Z drugiej strony istnieją różne formularze, gdzie każdy z nich jest unikalny. Ich wygląd najlepiej opisać za pomocą języka HTML w szablonie. I oczywiście oprócz obu wymienionych skrajności spotkamy wiele form, które mieszczą się gdzieś pomiędzy.
Rendering z Latte
System szablonów Latte zasadniczo ułatwia renderowanie formularzy i ich elementów. Najpierw pokażemy, jak ręcznie renderować formularze element po elemencie i dzięki temu zyskać pełną kontrolę nad kodem. Później pokażemy, jak zautomatyzować takie renderowanie.
Propozycję szablonu Latte dla formularza można wygenerować za pomocą metody
Nette\Forms\Blueprint::latte($form)
, która wyświetli go na stronie przeglądarki. Następnie wystarczy wybrać kod
jednym kliknięciem i skopiować go do swojego projektu.
{control}
Najprostszym sposobem renderowania formularza jest zapisanie go w szablonie:
{control signInForm}
Możesz wpłynąć na wygląd renderowanego formularza, konfigurując Renderer i poszczególne elementy.
n:name
Niezwykle łatwo jest połączyć definicję formularza w kodzie PHP z kodem HTML. Wystarczy dodać atrybuty
n:name
. To takie 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>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>
Forma powstałego kodu HTML jest całkowicie w Twoich rękach. Jeśli używasz atrybutu n:name
na elementach
<select>
, <button>
lub <textarea>
, ich wewnętrzna zawartość jest
automatycznie uzupełniana. Znacznik <form n:name>
dodatkowo tworzy zmienną lokalną $form
z obiektem formularza kreskówki i zamknięciem </form>
renderuje wszystkie niewykreślone elementy ukryte
(to samo dotyczy strony {form} ... {/form}
).
Nie możemy jednak zapominać o oddaniu ewentualnych komunikatów o błędach. Zarówno te dodane do elementów metodą
addError()
(przy użyciu {inputError}
), jak i te dodane bezpośrednio do formularza (zwrócone 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>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>
Bardziej złożone elementy formularza, takie jak RadioList lub CheckboxList, mogą być renderowane element po elemencie w ten sposób:
{foreach $form[gender]->getItems() as $key => $label}
<label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label>
{/foreach}
{label}
{input}
Dla każdego elementu nie chcesz myśleć o tym, jaki element HTML zastosować dla niego w szablonie, czy
<input>
, <textarea>
itp.? 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}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>
Jeśli formularz korzysta z translatora, tekst wewnątrz znaczników {label}
zostanie przetłumaczony.
Ponownie, bardziej złożone elementy formularza, takie jak RadioList lub CheckboxList, mogą być renderowane na podstawie poszczególnych pozycji:
{foreach $form[gender]->items as $key => $label}
{label gender:$key}{input gender:$key} {$label}{/label}
{/foreach}
Aby oddać rzeczywisty <input>
w elemencie Checkbox, należy użyć {input myCheckbox:}
. W tym
przypadku zawsze należy oddzielić atrybuty HTML przecinkiem {input myCheckbox:, class: required}
.
{inputError}
Wypisuje komunikat o błędzie dla elementu formularza, jeśli go posiada. Zazwyczaj zawijamy wiadomość w element HTML w
celu stylizacji. Unikanie renderowania pustego elementu, jeśli nie ma wiadomości, można elegancko zrobić z
n:ifcontent
:
<span class=error n:ifcontent>{inputError $input}</span>
Możemy wykryć obecność błędu za pomocą metody 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>
.
Automatyczne renderowanie
Dzięki znacznikom {input}
i {label}
możemy w prosty sposób stworzyć ogólny szablon dla dowolnego
formularza. Będzie on iterował i renderował wszystkie swoje elementy sekwencyjnie, z wyjątkiem elementów ukrytych, które
są renderowane automatycznie, gdy formularz zostanie zamknięty za pomocą znacznika </form>
. Będzie
oczekiwał nazwy wyrenderowanego 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 samozamykające się znaczniki parami {label .../}
pokazują etykiety pochodzące z definicji formularza w
kodzie PHP.
Na przykład zapisz ten ogólny szablon w pliku basic-form.latte
i aby wyrenderować formularz, wystarczy go
inline i przekazać nazwę formularza (lub instancję) do parametru $form
:
{include basic-form.latte, form: signInForm}
Jeśli chciałbyś ingerować w formularz podczas renderowania jednego konkretnego formularza, a być może renderować jeden element inaczej, to najprostszym sposobem jest posiadanie w szablonie gotowych bloków, które można później nadpisać. Bloki mogą mieć również dynamiczne nazwy, więc można do nich wstawić nazwę elementu, który ma być narysowany. Na przykład:
...
{label $input /}
{block "input-{$input->name}"}{input $input}{/block}
...
Dla elementu np. username
tworzy to blok input-username
, który można łatwo nadpisać za pomocą
znacznika {embed}:
{embed basic-form.latte, form: signInForm}
{block input-username}
<span class=important>
{include parent}
</span>
{/block}
{/embed}
Alternatywnie, cała zawartość szablonu basic-form.latte
może być zdefiniowana jako blok, łącznie z parametrem
$form
:
{define basic-form, $form}
<form n:name=$form class=form>
...
</form>
{/define}
Dzięki temu będzie nieco łatwiejszy do wywołania:
{embed basic-form, signInForm}
...
{/embed}
Blok wystarczy zaimportować tylko w jednym miejscu, na początku szablonu układu:
{import basic-form.latte}
Szczególne przypadki
Jeśli chcesz renderować tylko wewnętrzną część formularza bez znaczników HTML <form>
na przykład
podczas wysyłania fragmentów, ukryj je za pomocą atrybutu 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>
Znacznik {formContainer}
pomoże w rysowaniu elementów wewnątrz kontenera formularza.
<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}
Rendering bez Latte
Najprostszym sposobem renderowania formularza jest wywołanie:
$form->render();
Możesz wpłynąć na wygląd renderowanego formularza, konfigurując Renderer i poszczególne elementy.
Ręczne renderowanie
Każdy element formularza posiada metody, które generują kod HTML dla pola formularza i etykiet. Mogą one zwrócić go jako ciąg znaków lub obiekt Nette\Utils\Html:
getControl(): Html|string
zwraca kod HTML elementugetLabel($caption = null): Html|string|null
zwraca kod HTML etykiety, jeżeli taki istnieje
Formularz może być więc renderowany element po elemencie:
<?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 dla niektórych elementów getControl()
zwraca pojedynczy element HTML (np.
<input>
, <select>
itp.), dla innych cały fragment kodu HTML (CheckboxList, RadioList). W
takim przypadku możesz użyć metod, które generują indywidualne wejścia i etykiety, dla każdego elementu osobno:
getControlPart($key = null): ?Html
zwraca kod HTML jednego elementugetLabelPart($key = null): ?Html
zwraca kod HTML dla etykiety jednego elementu
Metody te mają przedrostek get
, ze względów historycznych, ale generate
byłby
lepszy, ponieważ tworzy i zwraca nowy element Html
przy każdym wywołaniu.
Renderer
Jest to obiekt, który renderuje formularz. Można go ustawić za pomocą metody $form->setRenderer
Jest
przekazywany kontroli, gdy wywoływana jest metoda $form->render()
.
Jeśli nie ustawimy niestandardowego renderera, zostanie użyty domyślny renderer Nette\Forms\Rendering\DefaultFormRenderer. Spowoduje to renderowanie elementów formularza jako tabeli HTML. Dane wyjściowe wyglądają tak:
<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>
...
To, czy używać tabeli do szkieletu formularza, czy nie, jest kontrowersyjne i wielu projektantów stron internetowych
preferuje inny narzut. Na przykład lista definicji. Dlatego przekonfigurujmy DefaultFormRenderer
, aby renderować
formularz jako listę. Konfiguracja odbywa się poprzez edycję pola $wrappers. Pierwszy
indeks zawsze reprezentuje region, a drugi jego atrybut. Poszczególne pola zostały przedstawione na rysunku:
Domyślnie grupa elementów controls
jest opakowana w tablicę <table>
, każdy
pair
reprezentuje wiersz tabeli <tr>
a pary label
i control
są
komórkami <th>
a <td>
. Teraz zmieniamy elementy opakowujące. Do pojemnika wkładamy obszar
controls
<dl>
, pozostawić obszar pair
niezamknięty, a label
umieścić
w <dt>
i wreszcie control
jest owinięty 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();
W rezultacie otrzymujemy taki oto kod HTML:
<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>
W polu wrappers można wpływać na szereg innych atrybutów:
- dodać klasy CSS do poszczególnych typów elementów formularza
- rozróżnienie między nieparzystymi i parzystymi wierszami według klasy CSS
- wizualne rozróżnienie elementów wymaganych i opcjonalnych
- określić, czy komunikaty o błędach są wyświetlane bezpośrednio przy elementach czy nad formularzem
Opcje
Zachowanie Renderera może być również kontrolowane poprzez ustawienie opcji na poszczególnych elementach formularza. W ten sposób można ustawić etykietę, która jest wyświetlana obok pola wejściowego:
$form->addText('phone', 'Číslo:')
->setOption('description', 'Toto číslo zůstane skryté');
Jeśli chcemy umieścić w nim treść HTML, używamy klasy 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>')
);
Zamiast etykiety można również użyć elementu Html:
$form->addCheckbox('conditions', $label)
.
Grupowanie elementów
Renderer pozwala na grupowanie elementów w wizualne grupy (fieldsets):
$form->addGroup('Personal data');
Po utworzeniu nowej grupy staje się ona aktywna i każdy nowo dodany element jest do niej dodawany. Zatem formularz można zbudować w ten sposób:
$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);
Program renderujący najpierw rysuje grupy, a następnie elementy, które nie należą do żadnej grupy.
Wsparcie dla Bootstrap
W przykładach znajdziesz przykłady jak skonfigurować Renderer dla Twitter Bootstrap 2, Bootstrap 3 i Bootstrap 4
Atrybuty HTML
Aby ustawić dowolne atrybuty HTML dla elementów formularza, należy użyć metody
setHtmlAttribute(string $name, $value = true)
:
$form->addInteger('liczba', 'Liczba:')
->setHtmlAttribute('class', 'big-number');
$form->addSelect('rank', 'Rank by:', ['cena', 'nazwa'])
->setHtmlAttribute('onchange', 'submit()'); // submit on change
// Aby ustawić atrybuty samej strony <form>
$form->setHtmlAttribute('id', 'myForm');
Określenie typu elementu:
$form->addText('tel', 'Váš telefon:')
->setHtmlType('tel')
->setHtmlAttribute('placeholder', 'napište telefon');
Ustawienie typu i innych atrybutów służy jedynie celom wizualnym. Weryfikacja poprawności danych wejściowych musi odbywać się na serwerze, co można zapewnić, wybierając odpowiednią kontrolkę formularza i określając reguły walidacji.
Dla poszczególnych elementów na listach radio lub checkbox możemy ustawić atrybut HTML z różnymi wartościami dla
każdego z nich. Zwróć uwagę na dwukropek po style:
, który zapewnia, że wartość jest wybierana na podstawie
klucza:
$colors = ['r' => 'červená', 'g' => 'zelená', 'b' => 'modrá'];
$styles = ['r' => 'background:red', 'g' => 'background:green'];
$form->addCheckboxList('colors', 'Barvy:', $colors)
->setHtmlAttribute('style:', $styles);
Listy:
<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>
Do ustawiania atrybutów logicznych, takich jak readonly
, możemy użyć notacji ze znakiem zapytania:
$form->addCheckboxList('colors', 'Colors:', $colors)
->setHtmlAttribute('readonly?', 'r'); // dla wielu kluczy należy użyć tablicy, np. ['r', 'g']
Listy:
<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>
W przypadku selectboxów metoda setHtmlAttribute()
ustawia atrybuty elementu <select>
. Jeśli
chcemy ustawić atrybuty dla poszczególnych <option>
używamy metody setOptionAttribute()
.
Powyższe zapisy z dwukropkiem i znakiem zapytania również działają:
$form->addSelect('colors', 'Barvy:', $colors)
->setOptionAttribute('style:', $styles);
Listy:
<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
Alternatywnym sposobem ustawiania atrybutów HTML jest modyfikacja szablonu, z którego generowany jest element HTML. Szablon
jest obiektem Html
i jest zwracany przez metodę getControlPrototype()
:
$input = $form->addInteger('number', 'Číslo:');
$html = $input->getControlPrototype(); // <input>
$html->class('big-number'); // <input class="big-number">
W ten sposób można również modyfikować szablon etykiety zwracany przez getLabelPrototype()
:
$html = $input->getLabelPrototype(); // <label>.
$html->class('distinctive'); // <label class="distinctive">.
W przypadku elementów Checkbox, CheckboxList i RadioList można wpływać na szablon elementu, który zawija element. Jest on
zwracany przez getContainerPrototype()
. Domyślnie jest to element “pusty”, więc nic nie jest renderowane, ale
nadając 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żna również wpływać na szablon separatora elementów zwracany przez metodę
getSeparatorPrototype()
. Domyślnie jest to element <br>
. Jeśli zmienisz go na element pary,
będzie on zawijał poszczególne elementy zamiast je oddzielać. I możesz również wpłynąć na szablon elementu HTML
elementu single-item label, który zwraca getItemLabelPrototype()
.
Tłumaczenie
Jeśli programujesz wielojęzyczną aplikację, prawdopodobnie będziesz musiał renderować formularz w różnych językach. Nette Framework definiuje w tym celu interfejs translacji Nette\Localization\Translator. W Nette nie ma domyślnej implementacji, możesz wybrać według swoich potrzeb spośród kilku gotowych rozwiązań, które znajdziesz na Componette. Ich dokumentacja podpowiada, jak skonfigurować translator.
Formularz obsługuje wyprowadzanie tekstu przez translator. Przekazujemy go za pomocą metody setTranslator()
:
$form->setTranslator($translator);
Od tej pory nie tylko wszystkie etykiety, ale także wszystkie komunikaty o błędach czy wpisy w polach wyboru będą tłumaczone na inny język.
Możliwe jest ustawienie innego tłumacza dla poszczególnych elementów formularza lub całkowite wyłączenie tłumaczenia za
pomocą null
:
$form->addSelect('carModel', 'Model:', $cars)
->setTranslator(null);
W przypadku reguł walidacji do tłumacza przekazywane są również określone parametry, na przykład dla reguły:
$form->addPassword('password', 'Password:')
->addRule($form::MinLength, 'Password has to be at least %d characters long', 8)
translator jest wywoływany z następującymi parametrami:
$translator->translate('Password has to be at least %d characters long', 8);
a więc może wybrać poprawną formę liczby mnogiej dla słowa characters
przez count.
Zdarzenie onRender
Tuż przed wyrenderowaniem formularza możemy zlecić wywołanie naszego kodu. Może to na przykład dodać klasy HTML do
elementów formularza w celu prawidłowego wyświetlania. Dodajemy kod do pola onRender
:
$form->onRender[] = function ($form) {
BootstrapCSS::initialize($form);
};