Rendering von Formularen

Das Aussehen von Formularen kann sehr unterschiedlich sein. In der Praxis begegnen uns zwei Extreme. Auf der einen Seite steht die Notwendigkeit, in einer Anwendung zahlreiche Formulare zu rendern, die optisch identisch sind, und wir wissen das mühelose Rendern ohne Template über $form->render() zu schätzen. Typisch ist das bei Verwaltungsoberflächen.

Auf der anderen Seite stehen vielfältige Formulare, von denen jedes einzigartig ist. Ihr Aussehen beschreibt man am besten mit HTML im Template des Formulars. Und natürlich begegnen uns neben diesen beiden Extremen viele Formulare, die irgendwo dazwischen liegen.

Rendering mit Latte

Das Templating-System Latte vereinfacht das Rendern von Formularen und ihren Elementen erheblich. Zuerst zeigen wir, wie sich ein Formular von Hand rendern lässt, Element für Element, um die volle Kontrolle über den Code zu haben. Später zeigen wir, wie sich ein solches Rendern automatisieren lässt.

Das Latte-Template für ein Formular können Sie sich über die Methode Nette\Forms\Blueprint::latte($form) erzeugen lassen, die es auf der Seite im Browser ausgibt. Dann markieren Sie den Code einfach mit einem Klick und kopieren ihn in Ihr Projekt.

{control}

Am einfachsten rendern Sie ein Formular, indem Sie ins Template schreiben:

{control signInForm}

Das Aussehen des gerenderten Formulars lässt sich über die Konfiguration des Renderer und der einzelnen Elemente beeinflussen.

n:name

Die Definition des Formulars im PHP-Code mit dem HTML-Code zu verbinden ist außerordentlich einfach. Ergänzen Sie einfach die Attribute n:name. So einfach ist das!

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>

Sie haben die volle Kontrolle über das Aussehen des entstehenden HTML-Codes. Verwenden Sie das Attribut n:name bei den Elementen <select>, <button> oder <textarea>, wird deren innerer Inhalt automatisch gefüllt. Außerdem erzeugt der Tag <form n:name> die lokale Variable $form mit dem Objekt des gerenderten Formulars, und der schließende Tag </form> rendert alle noch nicht gerenderten versteckten Elemente (dasselbe gilt für {form} ... {/form}).

Wir dürfen jedoch nicht vergessen, mögliche Fehlermeldungen auszugeben. Das betrifft sowohl Fehler, die den einzelnen Elementen über die Methode addError() hinzugefügt wurden (gerendert über {inputError}), als auch Fehler, die direkt dem Formular hinzugefügt wurden (die $form->getOwnErrors() zurückgibt):

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

Komplexere Formularelemente wie RadioList oder CheckboxList lassen sich Element für Element so rendern:

{foreach $form[gender]->getItems() as $key => $label}
	<label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label>
{/foreach}

{label} {input}

Sie möchten lieber nicht darüber nachdenken, welches HTML-Element Sie im Template für welches Formularelement verwenden, ob <input>, <textarea> und so weiter? Die Lösung ist der universelle 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>

Verwendet das Formular einen Übersetzer, werden die Labels aus der Definition des Formulars (etwa {label username /}) übersetzt. Text, der direkt zwischen den Tags {label} und {/label} steht, wird es nicht.

Auch hier lassen sich komplexere Formularelemente wie RadioList oder CheckboxList Element für Element rendern:

{foreach $form[gender]->items as $key => $label}
	{label gender:$key}{input gender:$key} {$label}{/label}
{/foreach}

Um bei einem Checkbox-Element nur das <input> zu rendern, verwenden Sie {input myCheckbox:}. Trennen Sie die HTML-Attribute in diesem Fall immer mit einem Komma: {input myCheckbox:, class: required}.

{inputError}

Gibt die Fehlermeldung eines Formularelements aus, sofern es eine gibt. Die Meldung wird üblicherweise in ein HTML-Element gepackt, damit sie sich stylen lässt. Dass bei fehlender Meldung kein leeres Element gerendert wird, erreichen Sie elegant mit n:ifcontent:

<span class=error n:ifcontent>{inputError $input}</span>

Ob ein Fehler vorliegt, prüfen wir über die Methode hasErrors() und setzen danach die Klasse des übergeordneten Elements:

<div n:class="$form[username]->hasErrors() ? 'error'">
	{input username}
	{inputError username}
</div>

{form}

Die Tags {form signInForm}...{/form} sind eine Alternative zu <form n:name="signInForm">...</form>. Trennen Sie etwaige Argumente vom Namen mit einem Komma: {form signInForm, class: foo}.

Das Schlüsselwort scope vor dem Namen legt das Formular nur auf den Stapel (sodass sich {input}, {label} und so weiter daran binden), rendert aber den Tag <form> nicht. Praktisch ist das, um einen Teil eines Formulars zu rendern, etwa in einem Snippet. Ist bereits ein Formular aktiv, wird der Name relativ dazu aufgelöst, {form scope} ersetzt also auch {formContainer}:

{form scope signInForm}
	{input username}
{/form}

Das Schlüsselwort detached rendert ein leeres <form></form> und verbindet jedes Element über das HTML-Attribut form damit. So können Sie ein Formular in ein anderes Formular setzen, was HTML sonst verbietet. Das losgelöste Formular muss ein HTML-id haben, das automatisch entsteht, wenn Sie ihm einen Namen geben (wie outerForm unten):

{form detached outerForm}
	...
{/form}

Automatisches Rendering

Dank der Tags {input} und {label} können wir leicht ein allgemeines Template für ein beliebiges Formular schreiben. Es durchläuft alle seine Elemente und rendert sie, ausgenommen die versteckten Elemente, die automatisch gerendert werden, wenn das Formular mit dem Tag </form> geschlossen wird. Es erwartet den Namen des zu rendernden Formulars in der Variablen $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>

Die hier verwendeten selbstschließenden Paar-Tags {label .../} geben die Labels aus, die aus der Definition des Formulars im PHP-Code stammen.

Speichern Sie dieses allgemeine Template zum Beispiel in der Datei basic-form.latte. Um ein Formular zu rendern, binden Sie sie einfach ein und übergeben den Namen des Formulars (oder die Instanz) im Parameter $form:

{include basic-form.latte, form: signInForm}

Wenn Sie das Aussehen eines bestimmten Formulars beim Rendern anpassen wollen, etwa ein Element anders rendern, bereiten Sie am einfachsten im Template Blöcke vor, die sich anschließend überschreiben lassen. Blöcke können auch dynamische Namen haben, sodass sich der Name des gerenderten Elements einsetzen lässt. Zum Beispiel:

...
	{label $input /}
	{block "input-{$input->name}"}{input $input}{/block}
...

Für ein Element namens etwa username entsteht so der Block input-username, der sich mit dem Tag {embed} leicht überschreiben lässt:

{embed basic-form.latte, form: signInForm}
	{block input-username}
		<span class=important>
			{include parent}
		</span>
	{/block}
{/embed}

Alternativ lässt sich der gesamte Inhalt des Templates basic-form.latte als Block definieren, samt dem Parameter $form:

{define basic-form, $form}
	<form n:name=$form class=form>
		...
	</form>
{/define}

Dadurch wird der Aufruf etwas einfacher:

{embed basic-form, signInForm}
	...
{/embed}

Den Block müssen Sie nur an einer Stelle importieren, am Anfang des Layout-Templates:

{import basic-form.latte}

Spezialfälle

Wenn Sie nur den inneren Teil des Formulars ohne die HTML-Tags <form> rendern müssen, etwa beim Senden von Snippets, verstecken Sie sie mit dem Attribut 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>

Beim Rendern der Elemente innerhalb eines Formular-Containers hilft der Tag {formContainer} oder das neuere {form scope}.

<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 ohne Latte

Am einfachsten rendern Sie ein Formular mit dem Aufruf:

$form->render();

Das Aussehen des gerenderten Formulars lässt sich über die Konfiguration des Renderer und der einzelnen Elemente beeinflussen.

Manuelles Rendering

Jedes Formularelement hat Methoden, die den HTML-Code des Formularfelds und seines Labels erzeugen. Sie können ihn entweder als String oder als Objekt Nette\Utils\Html zurückgeben:

  • getControl(): Html|string gibt den HTML-Code des Elements zurück
  • getLabel($caption = null): Html|string|null gibt den HTML-Code des Labels zurück, sofern es eines gibt

So lässt sich das Formular Element für Element rendern:

<?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') ?>

Während getControl() bei manchen Elementen ein einzelnes HTML-Element zurückgibt (etwa <input>, <select> und so weiter), liefert es bei anderen ein vollständiges Stück HTML-Code (CheckboxList, RadioList). In solchen Fällen können Sie Methoden verwenden, die die einzelnen Inputs und Labels für jedes Element getrennt erzeugen:

  • getControlPart($key = null): Html gibt den HTML-Code eines einzelnen Elements zurück
  • getLabelPart($key = null): Html gibt den HTML-Code des Labels eines einzelnen Elements zurück

Diese Methoden tragen aus historischen Gründen das Präfix get, passender wäre generate, denn sie erzeugen bei jedem Aufruf ein neues Html-Element und geben es zurück.

Renderer

Das ist ein Objekt, das für das Rendern des Formulars zuständig ist. Setzen lässt es sich über die Methode $form->setRenderer(). Die Kontrolle wird ihm beim Aufruf der Methode $form->render() übergeben.

Setzen wir keinen eigenen Renderer, wird der Standard-Renderer Nette\Forms\Rendering\DefaultFormRenderer verwendet. Er rendert die Formularelemente in eine HTML-Tabelle. Die Ausgabe sieht so aus:

<table>
<tr class="required">
	<th><label class="required" for="frm-name">Name:</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">Alter:</label></th>

	<td><input type="text" class="text" name="age" id="frm-age" required value=""></td>
</tr>

<tr>
	<th><label>Geschlecht:</label></th>
	...

Ob man für die Struktur eines Formulars eine Tabelle verwendet, ist umstritten, und viele Webdesigner bevorzugen ein anderes Markup, etwa eine Definitionsliste. Wir konfigurieren den DefaultFormRenderer deshalb so um, dass er das Formular als Liste rendert. Konfiguriert wird über das Bearbeiten des Arrays $wrappers. Der erste Index steht immer für einen Bereich, der zweite für dessen Attribut. Die einzelnen Bereiche zeigt das Bild:

Standardmäßig ist die Gruppe controls von <table> umschlossen, jedes pair stellt eine Tabellenzeile <tr> dar, und das Paar label und control sind die Zellen <th> und <td>. Jetzt ändern wir die umschließenden Elemente. Den Bereich controls setzen wir in einen Container <dl>, den Bereich pair lassen wir ohne Container, das label setzen wir in <dt> und das control umschließen wir schließlich mit den Tags <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();

Daraus entsteht der folgende HTML-Code:

<dl>
	<dt><label class="required" for="frm-name">Name:</label></dt>

	<dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd>


	<dt><label class="required" for="frm-age">Alter:</label></dt>

	<dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd>


	<dt><label>Geschlecht:</label></dt>
	...
</dl>

Über das Array wrappers lassen sich viele weitere Eigenschaften beeinflussen:

  • CSS-Klassen zu einzelnen Typen von Formularelementen hinzufügen
  • ungerade und gerade Zeilen über CSS-Klassen unterscheiden
  • Pflicht- und optionale Elemente optisch unterscheiden
  • festlegen, ob die Fehlermeldungen direkt neben den Elementen oder über dem Formular ausgegeben werden

Options

Das Verhalten des Renderers lässt sich auch über options an den einzelnen Formularelementen steuern. So setzen Sie eine Beschreibung, die neben dem Eingabefeld erscheint:

$form->addText('phone', 'Nummer:')
	->setOption('description', 'Diese Nummer bleibt verborgen');

Wollen wir HTML-Inhalt hineinsetzen, verwenden wir die Klasse Html:

use Nette\Utils\Html;

$form->addText('phone', 'Telefon:')
	->setOption('description', Html::el('p')
		->setHtml('<a href="...">Nutzungsbedingungen.</a>')
	);

Ein Html-Element lässt sich auch statt eines Labels verwenden: $form->addCheckbox('conditions', $label).

Gruppierung von Elementen

Der Renderer erlaubt es, Elemente zu optischen Gruppen (Fieldsets) zusammenzufassen:

$form->addGroup('Personal data');

Nach dem Anlegen einer neuen Gruppe wird diese aktiv, und jedes neu hinzugefügte Element wird auch ihr hinzugefügt. Das Formular lässt sich also so aufbauen:

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

Der Renderer zeichnet zuerst die Gruppen und danach die Elemente, die zu keiner Gruppe gehören.

Unterstützung für Bootstrap

Im Verzeichnis der Beispiele finden Sie Beispiele, wie sich der Renderer für Twitter Bootstrap 2, Bootstrap 3 und Bootstrap 4 konfigurieren lässt.

HTML-Attribute

Um beliebige HTML-Attribute von Formularelementen zu setzen, verwenden Sie die Methode setHtmlAttribute(string $name, $value = true):

$form->addInteger('number', 'Nummer:')
	->setHtmlAttribute('class', 'big-number');

$form->addSelect('rank', 'Sortieren nach:', ['Preis', 'Name'])
	->setHtmlAttribute('onchange', 'submit()'); // bei Änderung das Formular absenden


// Um die Attribute des <form>-Elements selbst zu setzen
$form->setHtmlAttribute('id', 'myForm');

Angabe des Typs eines Elements:

$form->addText('tel', 'Ihr Telefon:')
	->setHtmlType('tel')
	->setHtmlAttribute('placeholder', 'Bitte geben Sie Ihr Telefon an');

Das Setzen des Typs und weiterer Attribute dient nur der Optik. Die Prüfung der Richtigkeit der Eingaben muss auf der Serverseite geschehen, wofür Sie mit der Wahl eines passenden Formularelements und der Angabe von Validierungsregeln sorgen.

Bei den einzelnen Elementen von Radio- oder Checkbox-Listen können wir ein HTML-Attribut mit für jedes unterschiedlichen Werten setzen. Beachten Sie den Doppelpunkt hinter style:, der dafür sorgt, dass der Wert anhand des Schlüssels ausgewählt wird:

$colors = ['r' => 'rot', 'g' => 'grün', 'b' => 'blau'];
$styles = ['r' => 'background:red', 'g' => 'background:green'];
$form->addCheckboxList('colors', 'Farben:', $colors)
	->setHtmlAttribute('style:', $styles);

Gibt aus:

<label><input type="checkbox" name="colors[]" style="background:red" value="r">rot</label>
<label><input type="checkbox" name="colors[]" style="background:green" value="g">grün</label>
<label><input type="checkbox" name="colors[]" value="b">blau</label>

Für das Setzen boolescher Attribute wie readonly können wir die Schreibweise mit einem Fragezeichen verwenden:

$form->addCheckboxList('colors', 'Farben:', $colors)
	->setHtmlAttribute('readonly?', 'r'); // für mehrere Schlüssel ein Array verwenden, etwa ['r', 'g']

Gibt aus:

<label><input type="checkbox" name="colors[]" readonly value="r">rot</label>
<label><input type="checkbox" name="colors[]" value="g">grün</label>
<label><input type="checkbox" name="colors[]" value="b">blau</label>

Bei Select-Boxen setzt die Methode setHtmlAttribute() die Attribute des Elements <select>. Wollen wir die Attribute der einzelnen <option>-Elemente setzen, verwenden wir die Methode setOptionAttribute(). Die oben erwähnten Schreibweisen mit Doppelpunkt und Fragezeichen funktionieren auch hier:

$form->addSelect('colors', 'Farben:', $colors)
	->setOptionAttribute('style:', $styles);

Gibt aus:

<select name="colors">
	<option value="r" style="background:red">rot</option>
	<option value="g" style="background:green">grün</option>
	<option value="b">blau</option>
</select>

Prototypen

Ein alternativer Weg, HTML-Attribute zu setzen, ist, die Vorlage zu verändern, aus der das HTML-Element entsteht. Die Vorlage ist ein Html-Objekt und wird von der Methode getControlPrototype() zurückgegeben:

$input = $form->addInteger('number', 'Nummer:');
$html = $input->getControlPrototype(); // <input>
$html->class('big-number');            // <input class="big-number">

Ebenso lässt sich die Vorlage des Labels verändern, die getLabelPrototype() zurückgibt:

$html = $input->getLabelPrototype(); // <label>
$html->class('distinctive');         // <label class="distinctive">

Bei den Elementen Checkbox, CheckboxList und RadioList können Sie die Vorlage des Elements beeinflussen, das das gesamte Element umschließt. Sie gibt getContainerPrototype() zurück. Standardmäßig ist es ein “leeres” Element, sodass nichts gerendert wird; geben Sie ihm aber einen Namen, wird es gerendert:

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

Bei CheckboxList und RadioList können Sie außerdem die Vorlage des Trennzeichens der einzelnen Elemente beeinflussen, die die Methode getSeparatorPrototype() zurückgibt. Standardmäßig ist es das Element <br>. Ändern Sie es in ein Paar-Element, umschließt es die einzelnen Elemente, statt sie zu trennen. Ebenso können Sie die Vorlage des HTML-Elements für die Labels der einzelnen Elemente beeinflussen, die getItemLabelPrototype() zurückgibt.

Übersetzen

Wenn Sie eine mehrsprachige Anwendung entwickeln, müssen Sie das Formular vermutlich in verschiedenen Sprachversionen rendern. Das Nette Framework definiert dafür ein Interface für die Übersetzung: Nette\Localization\Translator. Nette hat keine Standardimplementierung; Sie können je nach Bedarf aus mehreren fertigen Lösungen wählen, die Sie auf Componette finden. In deren Dokumentation steht, wie Sie den Übersetzer einrichten.

Formulare unterstützen die Ausgabe von Texten über den Übersetzer. Wir übergeben ihn über die Methode setTranslator():

$form->setTranslator($translator);

Von diesem Moment an werden nicht nur alle Labels, sondern auch alle Fehlermeldungen, die Elemente von Select-Boxen und die Platzhalter der Eingabefelder in die Zielsprache übersetzt.

Für einzelne Formularelemente lässt sich ein anderer Übersetzer setzen oder die Übersetzung vollständig abschalten, indem der Wert auf null gesetzt wird:

$form->addSelect('carModel', 'Modell:', $cars)
	->setTranslator(null);

Bei den Validierungsregeln werden dem Übersetzer zusätzlich spezifische Parameter übergeben, zum Beispiel bei der Regel:

$form->addPassword('password', 'Passwort:')
	->addRule($form::MinLength, 'Das Passwort muss mindestens %d Zeichen lang sein', 8);

wird der Übersetzer mit diesen Parametern aufgerufen:

$translator->translate('Das Passwort muss mindestens %d Zeichen lang sein', 8);

und kann so anhand der Anzahl die richtige Pluralform für das Wort Zeichen wählen.

Event onRender

Kurz bevor das Formular gerendert wird, können wir eigenen Code aufrufen lassen. Dieser Code kann zum Beispiel den Formularelementen HTML-Klassen für die richtige Darstellung hinzufügen. Wir tragen den Code in das Array onRender ein:

$form->onRender[] = function ($form) {
	BootstrapCSS::initialize($form);
};
Version: 4.x