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 Rendererposzczegó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 Rendererposzczegó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 elementu
  • getLabel($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 pozycji
  • getLabelPart($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);

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);
};
wersja: 4.0