Rendu des formulaires
L'aspect des formes peut être très varié. Dans la pratique, nous pouvons rencontrer deux extrêmes. D'une part, il est
nécessaire de rendre une série de formulaires dans une application qui sont visuellement similaires les uns aux autres, et nous
apprécions le rendu facile sans modèle à l'aide de $form->render()
. C'est généralement le cas des interfaces
administratives.
D'autre part, il existe différents formulaires dont chacun est unique. Leur apparence est mieux décrite en utilisant le langage HTML dans le modèle. 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 fondamentalement le rendu des formulaires et de leurs éléments. Nous allons d'abord montrer comment rendre les formulaires manuellement, élément par élément, afin d'avoir un contrôle total sur le code. Plus tard, nous montrerons comment automatiser ce rendu.
Vous pouvez obtenir la proposition d'un modèle Latte pour le formulaire généré à l'aide de la
méthode Nette\Forms\Blueprint::latte($form)
, qui l'affichera sur la page du navigateur. Il vous suffit ensuite de
sélectionner le code d'un clic et de le copier dans votre projet.
{control}
La façon la plus simple de rendre un formulaire est d'écrire dans un modèle :
{control signInForm}
L'aspect du formulaire rendu peut être modifié en configurant le Renderer et les contrôles individuels.
n:name
Il est extrêmement facile de lier la définition du formulaire dans le code PHP avec le code HTML. Il suffit d'ajouter les
attributs n:name
. C'est aussi simple que cela !
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>
L'aspect du code HTML résultant est entièrement entre vos mains. Si vous utilisez l'attribut n:name
avec
<select>
, <button>
ou <textarea>
leur contenu interne est automatiquement
rempli. En outre, la balise <form n:name>
crée une variable locale $form
avec l'objet de
formulaire dessiné et la balise de fermeture </form>
dessine tous les éléments cachés non dessinés (il en
va de même pour {form} ... {/form}
).
Il ne faut cependant pas oublier de rendre les éventuels messages d'erreur. Aussi bien ceux qui ont été ajoutés aux
éléments individuels par la méthode addError()
(en utilisant {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 élément par élément :
{foreach $form[gender]->getItems() as $key => $label}
<label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label>
{/foreach}
{label}
{input}
Ne voulez-vous pas penser pour chaque élément à l'élément HTML à utiliser pour lui dans le modèle, 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 contenu dans les balises {label}
sera traduit.
Là encore, les éléments de formulaire plus complexes, tels que RadioList ou CheckboxList, peuvent être rendus élément par élément :
{foreach $form[gender]->items as $key => $label}
{label gender:$key}{input gender:$key} {$label}{/label}
{/foreach}
Pour rendre l'élément <input>
dans l'élément Checkbox, utilisez {input myCheckbox:}
. Les
attributs HTML doivent être séparés par une virgule {input myCheckbox:, class: required}
.
{inputError}
Imprime un 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. Pour éviter de rendre un élément vide s'il n'y a pas de message, il est possible de le faire de
manière élégante avec n:ifcontent
:
<span class=error n:ifcontent>{inputError $input}</span>
Nous pouvons détecter la présence d'une erreur à l'aide de la méthode hasErrors()
et définir la classe de
l'élément parent en conséquence :
<div n:class="$form[username]->hasErrors() ? 'error'">
{input username}
{inputError username}
</div>
{form}
Les étiquettes {form signInForm}...{/form}
sont une alternative à
<form n:name="signInForm">...</form>
.
Rendu automatique
Avec les balises {input}
et {label}
, nous pouvons facilement créer un modèle générique pour
n'importe quel formulaire. Il itérera et rendra tous ses éléments séquentiellement, à l'exception des éléments cachés, qui
sont rendus automatiquement lorsque le formulaire est terminé par la balise </form>
. Il attendra le nom du
formulaire rendu 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 à paire auto-fermante utilisées {label .../}
affichent les étiquettes provenant de la définition
du formulaire dans le code PHP.
Vous pouvez enregistrer ce modèle générique dans le fichier basic-form.latte
et pour rendre le formulaire, il
suffit de l'inclure et de passer le nom du formulaire (ou son instance) au paramètre $form
:
{include basic-form.latte, form: signInForm}
Si vous souhaitez influencer l'apparence d'un formulaire particulier et dessiner un élément différemment, le moyen le plus simple est de préparer des blocs dans le modèle qui peuvent être écrasés ultérieurement. Les blocs peuvent également avoir des noms dynamiques, vous pouvez donc y insérer le nom de l'élément à dessiner. Par exemple :
...
{label $input /}
{block "input-{$input->name}"}{input $input}{/block}
...
Pour l'élément username
, cela crée le bloc input-username
, qui peut facilement être remplacé par
la balise {embed}:
{embed basic-form.latte, form: signInForm}
{block input-username}
<span class=important>
{include parent}
</span>
{/block}
{/embed}
Par ailleurs, tout le contenu du modèle 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}
Cela rendra son utilisation légèrement plus facile :
{embed basic-form, signInForm}
...
{/embed}
Vous ne devez importer le bloc qu'à un seul endroit, au début du modèle de mise en page :
{import basic-form.latte}
Cas particuliers
Si vous devez rendre uniquement la partie intérieure du formulaire sans balises HTML, par exemple lors de l'envoi d'extraits,
masquez-les à l'aide de l'attribut . <form>
par exemple lors de l'envoi d'extraits, 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 à rendre les entrées à l'intérieur d'un conteneur de formulaire.
<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}
Rendu sans Latte
La façon la plus simple de rendre un formulaire est d'appeler :
$form->render();
L'apparence du formulaire rendu peut être modifiée en configurant le Renderer et les contrôles individuels.
Rendu manuel
Chaque élément de formulaire possède des méthodes qui génèrent le code HTML pour le champ de formulaire et l'étiquette. Elles peuvent le renvoyer sous la forme d'une chaîne ou d'un objet Nette\Utils\Html:
getControl(): Html|string
renvoie le code HTML de l'élémentgetLabel($caption = null): Html|string|null
renvoie le code HTML de l'étiquette, le cas échéant.
Cela permet de rendre le formulaire é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()
renvoie un seul élément HTML (par ex.
<input>
, <select>
etc.), pour d'autres, il renvoie un morceau entier de code HTML
(CheckboxList, RadioList). Dans ce cas, vous pouvez utiliser des méthodes qui génèrent des entrées et des étiquettes
individuelles, pour chaque élément séparément :
getControlPart($key = null): ?Html
renvoie le code HTML d'un seul élémentgetLabelPart($key = null): ?Html
renvoie le code HTML de l'étiquette d'un seul élément.
Ces méthodes sont préfixées par get
pour des raisons historiques, mais generate
serait plus approprié, car il crée et renvoie un nouvel élément Html
à chaque appel.
Renderer
C'est un objet qui assure le rendu du formulaire. Il peut être défini par la méthode $form->setRenderer
. Le
contrôle lui est transmis lorsque la méthode $form->render()
est appelée.
Si nous ne définissons pas de moteur de rendu personnalisé, le moteur de rendu par défaut Nette\Forms\Rendering\DefaultFormRenderer sera utilisé. Il rendra les éléments du formulaire sous la forme d'un tableau HTML. Le résultat ressemble à ceci :
<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">Age:</label></th>
<td><input type="text" class="text" name="age" id="frm-age" required value=""></td>
</tr>
<tr>
<th><label>Gender:</label></th>
...
C'est à vous de décider si vous voulez utiliser un tableau ou non, et de nombreux concepteurs de sites Web préfèrent des
balises différentes, par exemple une liste. Nous pouvons configurer DefaultFormRenderer
pour qu'il ne soit pas rendu
dans un tableau du tout. Il suffit de définir les $wrappers
appropriés. Le premier indice représente toujours une zone et le second un élément. Toutes les zones respectives sont
indiquées dans l'image :
Par défaut, un groupe de controls
est entouré de <table>
et chaque pair
est une
ligne de tableau <tr>
contenant une paire de label
et control
(cellules
<th>
et <td>
). Changeons tous ces éléments d'habillage. Nous allons envelopper
controls
dans <dl>
laisser pair
seul, mettre label
dans
<dt>
et transformer control
en <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();
Ce qui donne l'extrait suivant :
<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">Age:</label></dt>
<dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd>
<dt><label>Gender:</label></dt>
...
</dl>
Les wrappers peuvent affecter de nombreux attributs. Par exemple :
- ajouter des classes CSS spéciales à chaque entrée du formulaire
- distinguer les lignes paires et impaires
- faire en sorte que les éléments obligatoires et facultatifs soient dessinés différemment
- déterminer si les messages d'erreur doivent être affichés au-dessus du formulaire ou près de chaque élément.
Options
Le comportement du Renderer peut également être contrôlé en définissant des options sur des éléments de formulaire individuels. Ainsi, vous pouvez définir l'infobulle qui s'affiche à côté du champ de saisie :
$form->addText('phone', 'Number:')
->setOption('description', 'This number will remain hidden');
Si nous voulons y placer du contenu HTML, nous utilisons la classe Html.
use Nette\Utils\Html;
$form->addText('phone', 'Phone:')
->setOption('description', Html::el('p')
->setHtml('<a href="...">Terms of service.</a>')
);
L'élément Html peut également être utilisé à la place de l'étiquette :
$form->addCheckbox('conditions', $label)
.
Regroupement des entrées
Le Renderer permet de regrouper des éléments en groupes visuels (fieldsets) :
$form->addGroup('Personal data');
La création d'un nouveau groupe l'active – tous les éléments ajoutés par la suite sont ajoutés à ce groupe. Vous pouvez construire un formulaire comme ceci :
$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);
Le moteur de rendu dessine d'abord les groupes, puis les éléments qui n'appartiennent à aucun groupe.
Support Bootstrap
Vous trouverez des exemples de configuration du moteur de rendu 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', 'Number:')
->setHtmlAttribute('class', 'big-number');
$form->addSelect('rank', 'Order by:', ['price', 'name'])
->setHtmlAttribute('onchange', 'submit()'); // appelle la fonction JS submit() en cas de modification.
// Pour définir les attributs du site <form> lui-même
$form->setHtmlAttribute('id', 'myForm');
Spécification du type d'élément :
$form->addText('tel', 'Your telephone:')
->setHtmlType('tel')
->setHtmlAttribute('placeholder', 'Please, fill in your telephone');
La définition du type et d'autres attributs ne sert qu'à des fins visuelles. La vérification de l'exactitude des entrées doit avoir lieu sur le serveur, ce que vous pouvez faire en choisissant un contrôle de formulaire approprié et en spécifiant des règles de validation.
Pour les éléments individuels des listes de radios ou de cases à cocher, nous pouvons définir un attribut HTML avec des
valeurs différentes pour chacun d'entre eux. Remarquez les deux points après style:
, qui garantissent que la valeur
est sélectionnée en fonction de la clé :
$colors = ['r' => 'red', 'g' => 'green', 'b' => 'blue'];
$styles = ['r' => 'background:red', 'g' => 'background:green'];
$form->addCheckboxList('colors', 'Colors:', $colors)
->setHtmlAttribute('style:', $styles);
Renders :
<label><input type="checkbox" name="colors[]" style="background:red" value="r">red</label>
<label><input type="checkbox" name="colors[]" style="background:green" value="g">green</label>
<label><input type="checkbox" name="colors[]" value="b">blue</label>
Pour définir des attributs booléens, tels que readonly
, nous pouvons utiliser la notation avec un point
d'interrogation :
$form->addCheckboxList('colors', 'Colors:', $colors)
->setHtmlAttribute('readonly?', 'r'); // utiliser un tableau pour les clés multiples, par exemple ['r', 'g'].
Rendu :
<label><input type="checkbox" name="colors[]" readonly value="r">red</label>
<label><input type="checkbox" name="colors[]" value="g">green</label>
<label><input type="checkbox" name="colors[]" value="b">blue</label>
Pour les boîtes de sélection, la méthode setHtmlAttribute()
définit les attributs de l'élément
<select>
de l'élément. Si nous voulons définir les attributs pour chaque <option>
nous
utiliserons la méthode setOptionAttribute()
. De même, les deux points et le point d'interrogation utilisés
ci-dessus fonctionnent :
$form->addSelect('colors', 'Colors:', $colors)
->setOptionAttribute('style:', $styles);
Rendu :
<select name="colors">
<option value="r" style="background:red">red</option>
<option value="g" style="background:green">green</option>
<option value="b">blue</option>
</select>
Prototypes
Une autre façon 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 renvoyé par la méthode getControlPrototype()
:
$input = $form->addInteger('number');
$html = $input->getControlPrototype(); // <input>
$html->class('big-number'); // <input class="big-number">
Le modèle d'étiquette renvoyé par getLabelPrototype()
peut également être modifié de cette manière :
$html = $input->getLabelPrototype(); // <label>
$html->class('distinctif'); // <label class="distinctif">
Pour les éléments Checkbox, CheckboxList et RadioList, vous pouvez influencer le modèle d'élément qui enveloppe
l'élément. Il est renvoyé par getContainerPrototype()
. Par défaut, c'est un élément “vide”, donc rien n'est
rendu, mais en lui donnant 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 des CheckboxList et RadioList, il est également possible d'influencer le modèle de séparateur d'éléments
renvoyé par la méthode getSeparatorPrototype()
. Par défaut, il s'agit d'un élément <br>
. Si
vous le changez en un élément de paire, il enveloppera les éléments individuels au lieu de les séparer. Il est également
possible d'influencer le modèle d'élément HTML des étiquettes d'éléments, qui renvoie
getItemLabelPrototype()
.
Traduire
Si vous programmez une application multilingue, vous aurez probablement besoin de rendre le formulaire dans différentes langues. Le Framework Nette définit une interface de traduction à cet effet Nette\Localization\Translator. Il n'y a pas d'implémentation par défaut dans Nette, vous pouvez choisir en fonction de vos besoins parmi plusieurs solutions prêtes à l'emploi que vous pouvez trouver sur Componette. Leur documentation vous indique comment configurer le traducteur.
Le formulaire supporte la sortie de texte par le traducteur. Nous le passons en utilisant la méthode
setTranslator()
:
$form->setTranslator($translator);
À partir de maintenant, non seulement toutes les étiquettes, mais aussi tous les messages d'erreur ou les entrées des boîtes de sélection seront traduits dans une autre langue.
Il est possible de définir un traducteur différent pour chaque élément de formulaire ou de désactiver complètement la
traduction à l'aide de null
:
$form->addSelect('carModel', 'Model:', $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', 'Password:')
->addRule($form::MinLength, 'Password has to be at least %d characters long', 8)
le traducteur est appelé avec les paramètres suivants :
$translator->translate('Password has to be at least %d characters long', 8);
et peut ainsi choisir la forme plurielle correcte pour le mot characters
par comptage.
Événement onRender
Juste avant que le formulaire ne soit rendu, nous pouvons faire appel à notre code. Celui-ci peut, par exemple, ajouter des
classes HTML aux éléments du formulaire pour un affichage correct. Nous ajoutons le code au tableau onRender
:
$form->onRender[] = function ($form) {
BootstrapCSS::initialize($form);
};