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

Możemy ustawić dowolne atrybuty HTML do elementów formularza za pomocą 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


// jeśli chcemy zrobić to samo dla <form>
$form->setHtmlAttribute('id', 'myForm');

Ustawienia typu:

$form->addText('tel', 'Váš telefon:')
	->setHtmlType('tel')
	->setHtmlAttribute('placeholder', 'napište telefon');

Możemy ustawić atrybut HTML dla poszczególnych pozycji w listach radio lub checkbox z różnymi wartościami dla każdej z nich. Zwróć uwagę na dwukropek po style:, który zapewni, że wartość zostanie wybrana zgodnie z kluczem:

$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>

Jeśli jest to logiczny atrybut HTML (który nie ma wartości, np. readonly), możemy użyć notacji znaku zapytania:

$colors = ['r' => 'red', 'g' => 'green', 'b' => 'blue'];
$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');
echo $input->getControl();
// <label><input type="checkbox" name="send"></label>.

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