Rendu des formulaires

L'apparence des formulaires peut être très variée. En pratique, nous pouvons rencontrer deux extrêmes. D'un côté, il y a le besoin de rendre dans l'application de nombreux formulaires qui se ressemblent visuellement comme deux gouttes d'eau, et nous apprécierons un rendu facile sans template à l'aide de $form->render(). C'est généralement le cas des interfaces d'administration.

De l'autre côté, il y a des formulaires variés où la règle est : chaque pièce est originale. Leur forme est mieux décrite par le langage HTML dans le template du formulaire. Et bien sûr, en plus des deux extrêmes mentionnés, nous rencontrerons de nombreux formulaires qui se situent quelque part entre les deux.

Rendu avec Latte

Le Système de templates Latte facilite grandement le rendu des formulaires et de leurs éléments. Nous allons d'abord montrer comment rendre les formulaires manuellement, élément par élément, et ainsi obtenir un contrôle total sur le code. Plus tard, nous montrerons comment ce rendu peut être automatisé.

Vous pouvez faire générer le design du template Latte du formulaire à l'aide de la méthode Nette\Forms\Blueprint::latte($form), qui l'affichera dans la page du navigateur. Il suffit ensuite de cliquer pour sélectionner le code et de le copier dans le projet.

{control}

La manière la plus simple de rendre un formulaire est d'écrire dans le template :

{control signInForm}

L'apparence du formulaire ainsi rendu peut être influencée par la configuration du Renderer et des éléments individuels.

n:name

La définition du formulaire dans le code PHP peut être très facilement liée au code HTML. Il suffit d'ajouter des attributs n:name. C'est aussi simple que ça !

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>

Vous avez un contrôle total sur la forme du code HTML résultant. Si vous utilisez l'attribut n:name sur les éléments <select>, <button> ou <textarea>, leur contenu interne sera automatiquement complété. La balise <form n:name> crée en outre une variable locale $form avec l'objet du formulaire dessiné et la fermeture </form> rend tous les éléments cachés non rendus (il en va de même pour {form} ... {/form}).

Nous ne devons cependant pas oublier de rendre les éventuels messages d'erreur. Aussi bien ceux qui ont été ajoutés aux éléments individuels avec la méthode addError() (à l'aide de {inputError}), que ceux ajoutés directement au formulaire (renvoyés par $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>

Les éléments de formulaire plus complexes, tels que RadioList ou CheckboxList, peuvent être rendus ainsi par éléments individuels :

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

{label} {input}

Vous ne voulez pas réfléchir pour chaque élément à quel élément HTML utiliser dans le template, que ce soit <input>, <textarea>, etc. ? La solution est la balise universelle {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>

Si le formulaire utilise un traducteur, le texte à l'intérieur des balises {label} sera traduit.

Même dans ce cas, les éléments de formulaire plus complexes, tels que RadioList ou CheckboxList, peuvent être rendus par éléments individuels :

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

Pour rendre uniquement le <input> dans l'élément Checkbox, utilisez {input myCheckbox:}. Les attributs HTML dans ce cas doivent toujours être séparés par une virgule {input myCheckbox:, class: required}.

{inputError}

Affiche le message d'erreur pour l'élément de formulaire, s'il en a un. Le message est généralement enveloppé dans un élément HTML pour le style. Empêcher le rendu d'un élément vide si le message n'existe pas peut être fait élégamment avec n:ifcontent :

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

La présence d'une erreur peut être vérifiée avec la méthode hasErrors() et en fonction de cela, définir une classe sur l'élément parent :

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

{form}

Les balises {form signInForm}...{/form} sont une alternative à <form n:name="signInForm">...</form>.

Rendu automatique

Grâce aux balises {input} et {label}, nous pouvons facilement créer un template générique pour n'importe quel formulaire. Il itérera et rendra progressivement tous ses éléments, à l'exception des éléments cachés, qui seront rendus automatiquement à la fermeture du formulaire par la balise </form>. Le nom du formulaire à rendre sera attendu dans la variable $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>

Les balises paires auto-fermantes {label .../} utilisées affichent les étiquettes provenant de la définition du formulaire dans le code PHP.

Enregistrez ce template générique par exemple dans le fichier basic-form.latte et pour rendre le formulaire, il suffit de l'inclure et de passer le nom (ou l'instance) du formulaire au paramètre $form :

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

Si vous souhaitez intervenir dans l'apparence d'un formulaire particulier lors de son rendu et, par exemple, rendre un élément différemment, le moyen le plus simple est de préparer des blocs dans le template qui pourront être ensuite surchargés. Les blocs peuvent également avoir des noms dynamiques, on peut donc y insérer le nom de l'élément rendu. Par exemple :

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

Pour l'élément, par exemple username, un bloc input-username sera créé, qui peut être facilement surchargé en utilisant la balise {embed} :

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

Alternativement, tout le contenu du template basic-form.latte peut être défini comme un bloc, y compris le paramètre $form :

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

Grâce à cela, son appel sera légèrement plus simple :

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

Il suffit d'importer le bloc à un seul endroit, au début du template de layout :

{import basic-form.latte}

Cas spéciaux

Si vous avez besoin de rendre uniquement la partie interne du formulaire sans les balises HTML <form>, par exemple lors de l'envoi de snippets, masquez-les à l'aide de l'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>

La balise {formContainer} aide au rendu des éléments à l'intérieur d'un conteneur de formulaire.

<p>Quelles nouvelles souhaitez-vous recevoir :</p>

{formContainer emailNews}
<ul>
	<li>{input sport} {label sport /}</li>
	<li>{input science} {label science /}</li>
</ul>
{/formContainer}

Rendu sans Latte

La manière la plus simple de rendre un formulaire est d'appeler :

$form->render();

L'apparence du formulaire ainsi rendu peut être influencée par la configuration du Renderer et des éléments individuels.

Rendu manuel

Chaque élément de formulaire dispose de méthodes qui génèrent le code HTML du champ de formulaire et de l'étiquette. Elles peuvent le retourner soit sous forme de chaîne de caractères, soit sous forme d'objet Nette\Utils\Html :

  • getControl(): Html|string retourne le code HTML de l'élément
  • getLabel($caption = null): Html|string|null retourne le code HTML de l'étiquette, si elle existe

Le formulaire peut ainsi être rendu élément par élément :

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

Alors que pour certains éléments, getControl() retourne un seul élément HTML (par ex. <input>, <select>, etc.), pour d'autres, il retourne un morceau entier de code HTML (CheckboxList, RadioList). Dans ce cas, vous pouvez utiliser des méthodes qui génèrent des inputs et des étiquettes individuels, pour chaque élément séparément :

  • getControlPart($key = null): ?Html retourne le code HTML d'un élément individuel
  • getLabelPart($key = null): ?Html retourne le code HTML de l'étiquette d'un élément individuel

Ces méthodes ont pour des raisons historiques le préfixe get, mais generate serait meilleur, car à chaque appel, elles créent et retournent un nouvel élément Html.

Renderer

C'est un objet assurant le rendu du formulaire. Il peut être défini avec la méthode $form->setRenderer. Le contrôle lui est transmis lors de l'appel de la méthode $form->render().

Si nous ne définissons pas notre propre renderer, le renderer par défaut Nette\Forms\Rendering\DefaultFormRenderer sera utilisé. Celui-ci rend les éléments du formulaire sous forme de tableau HTML. La sortie ressemble à ceci :

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

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

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

L'utilisation ou non d'un tableau pour la structure du formulaire est discutable et de nombreux webdesigners préfèrent un autre balisage. Par exemple, une liste de définitions. Nous allons donc reconfigurer DefaultFormRenderer pour qu'il rende le formulaire sous forme de liste. La configuration se fait en modifiant le tableau $wrappers. Le premier index représente toujours la zone et le second son attribut. Les différentes zones sont illustrées par l'image :

Par défaut, le groupe d'éléments controls est enveloppé dans un tableau <table>, chaque pair représente une ligne de tableau <tr> et la paire label et control sont les cellules <th> et <td>. Nous allons maintenant changer les éléments enveloppants. Nous insérons la zone controls dans un conteneur <dl>, laissons la zone pair sans conteneur, insérons label dans <dt> et enfin enveloppons control avec les balises <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();

Le résultat est ce code HTML :

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

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


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

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


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

Dans le tableau wrappers, on peut influencer de nombreux autres attributs :

  • ajouter des classes CSS aux types individuels d'éléments de formulaire
  • distinguer par une classe CSS les lignes paires et impaires
  • distinguer visuellement les éléments obligatoires et facultatifs
  • déterminer si les messages d'erreur s'affichent directement à côté des éléments ou au-dessus du formulaire

Options

Le comportement du Renderer peut également être contrôlé en définissant des options sur les éléments de formulaire individuels. On peut ainsi définir une description qui sera affichée à côté du champ de saisie :

$form->addText('phone', 'Numéro :')
	->setOption('description', 'Ce numéro restera caché');

Si nous voulons y placer du contenu HTML, nous utilisons la classe Html

use Nette\Utils\Html;

$form->addText('phone', 'Numéro :')
	->setOption('description', Html::el('p')
		->setHtml('<a href="...">Conditions de conservation de votre numéro</a>')
	);

L'élément Html peut également être utilisé à la place de l'étiquette : $form->addCheckbox('conditions', $label).

Regroupement d'éléments

Le Renderer permet de regrouper les éléments en groupes visuels (fieldsets) :

$form->addGroup('Données personnelles');

Après la création d'un nouveau groupe, celui-ci devient actif et chaque nouvel élément ajouté est également ajouté à ce groupe. Le formulaire peut donc être construit de cette manière :

$form = new Form;
$form->addGroup('Données personnelles');
$form->addText('name', 'Votre nom :');
$form->addInteger('age', 'Votre âge :');
$form->addEmail('email', 'Email :');

$form->addGroup('Adresse de livraison');
$form->addCheckbox('send', 'Expédier à l\'adresse');
$form->addText('street', 'Rue :');
$form->addText('city', 'Ville :');
$form->addSelect('country', 'Pays :', $countries);

Le Renderer rend d'abord les groupes, puis les éléments qui n'appartiennent à aucun groupe.

Support pour Bootstrap

Dans les exemples, vous trouverez des exemples sur la façon de configurer le Renderer pour Twitter Bootstrap 2, Bootstrap 3 et Bootstrap 4

Attributs HTML

Pour définir des attributs HTML arbitraires pour les éléments de formulaire, utilisez la méthode setHtmlAttribute(string $name, $value = true) :

$form->addInteger('number', 'Numéro :')
	->setHtmlAttribute('class', 'big-number');

$form->addSelect('rank', 'Trier par :', ['prix', 'nom'])
	->setHtmlAttribute('onchange', 'submit()'); // envoyer lors du changement


// Pour définir les attributs de <form> lui-même
$form->setHtmlAttribute('id', 'myForm');

Spécification du type d'élément :

$form->addText('tel', 'Votre téléphone :')
	->setHtmlType('tel')
	->setHtmlAttribute('placeholder', 'écrivez le téléphone');

La définition du type et d'autres attributs sert uniquement à des fins visuelles. La vérification de l'exactitude des entrées doit avoir lieu côté serveur, ce que vous assurez en choisissant un élément de formulaire approprié et en indiquant des règles de validation.

Nous pouvons définir des attributs HTML avec des valeurs différentes pour chacun des éléments individuels dans les listes radio ou checkbox. Notez les deux-points après style:, qui assurent le choix de la valeur selon la clé :

$colors = ['r' => 'rouge', 'g' => 'vert', 'b' => 'bleu'];
$styles = ['r' => 'background:red', 'g' => 'background:green'];
$form->addCheckboxList('colors', 'Couleurs :', $colors)
	->setHtmlAttribute('style:', $styles);

Affiche :

<label><input type="checkbox" name="colors[]" style="background:red" value="r">rouge</label>
<label><input type="checkbox" name="colors[]" style="background:green" value="g">vert</label>
<label><input type="checkbox" name="colors[]" value="b">bleu</label>

Pour définir des attributs logiques, comme readonly, nous pouvons utiliser la notation avec un point d'interrogation :

$form->addCheckboxList('colors', 'Couleurs :', $colors)
	->setHtmlAttribute('readonly?', 'r'); // pour plusieurs clés, utilisez un tableau, par ex. ['r', 'g']

Affiche :

<label><input type="checkbox" name="colors[]" readonly value="r">rouge</label>
<label><input type="checkbox" name="colors[]" value="g">vert</label>
<label><input type="checkbox" name="colors[]" value="b">bleu</label>

Dans le cas des selectbox, la méthode setHtmlAttribute() définit les attributs de l'élément <select>. Si nous voulons définir les attributs des <option> individuels, nous utilisons la méthode setOptionAttribute(). Les notations avec deux-points et point d'interrogation mentionnées ci-dessus fonctionnent également :

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

Affiche :

<select name="colors">
	<option value="r" style="background:red">rouge</option>
	<option value="g" style="background:green">vert</option>
	<option value="b">bleu</option>
</select>

Prototypes

Une manière alternative de définir les attributs HTML consiste à modifier le modèle à partir duquel l'élément HTML est généré. Le modèle est un objet Html et est retourné par la méthode getControlPrototype() :

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

De cette manière, on peut également modifier le modèle de l'étiquette, retourné par getLabelPrototype() :

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

Pour les éléments Checkbox, CheckboxList et RadioList, vous pouvez influencer le modèle de l'élément qui enveloppe l'élément entier. Il est retourné par getContainerPrototype(). Par défaut, il s'agit d'un élément “vide”, donc rien n'est rendu, mais en lui définissant un nom, il sera rendu :

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

Dans le cas de CheckboxList et RadioList, on peut également influencer le modèle du séparateur des éléments individuels, retourné par la méthode getSeparatorPrototype(). Par défaut, c'est l'élément <br>. Si vous le changez en un élément pair, il enveloppera les éléments individuels au lieu de les séparer. Et de plus, on peut influencer le modèle de l'élément HTML de l'étiquette pour les éléments individuels, retourné par getItemLabelPrototype().

Traduction

Si vous programmez une application multilingue, vous aurez probablement besoin de rendre le formulaire dans différentes versions linguistiques. Nette Framework définit à cet effet une interface pour la traduction Nette\Localization\Translator. Il n'y a pas d'implémentation par défaut dans Nette, vous pouvez choisir parmi plusieurs solutions prêtes à l'emploi selon vos besoins, que vous trouverez sur Componette. Dans leur documentation, vous apprendrez comment configurer le traducteur.

Les formulaires prennent en charge l'affichage de textes via un traducteur. Nous le leur passons à l'aide de la méthode setTranslator() :

$form->setTranslator($translator);

À partir de ce moment, non seulement toutes les étiquettes, mais aussi tous les messages d'erreur ou les éléments des select box seront traduits dans une autre langue.

Pour les éléments de formulaire individuels, il est possible de définir un traducteur différent ou de désactiver complètement la traduction avec la valeur null :

$form->addSelect('carModel', 'Modèle :', $cars)
	->setTranslator(null);

Pour les règles de validation, des paramètres spécifiques sont également transmis au traducteur, par exemple pour la règle :

$form->addPassword('password', 'Mot de passe :')
	->addRule($form::MinLength, 'Le mot de passe doit comporter au moins %d caractères', 8);

le traducteur est appelé avec ces paramètres :

$translator->translate('Le mot de passe doit comporter au moins %d caractères', 8);

et peut donc choisir la forme correcte du pluriel pour le mot caractères en fonction du nombre.

Événement onRender

Juste avant que le formulaire ne soit rendu, nous pouvons faire exécuter notre code. Celui-ci peut par exemple ajouter des classes HTML aux éléments de formulaire pour un affichage correct. Nous ajoutons le code au tableau onRender :

$form->onRender[] = function ($form) {
	BootstrapCSS::initialize($form);
};
version: 4.0