Rendering dei form
L'aspetto dei form può essere molto vario. Nella pratica possiamo incontrare due estremi. Da un lato c'è la necessità di
disegnare in un'applicazione numerosi form visivamente identici, e apprezziamo il rendering semplice senza template con
$form->render(). È il caso tipico delle interfacce di amministrazione.
Dall'altro lato ci sono i form più diversi, ognuno dei quali è unico. Il loro aspetto si descrive al meglio con l'HTML nel template del form. E naturalmente, oltre a questi due estremi, incontriamo molti form che stanno da qualche parte nel mezzo.
Rendering con Latte
Il sistema di template Latte semplifica notevolmente il rendering dei form e dei loro elementi. Mostreremo prima come disegnare i form manualmente, elemento per elemento, per avere il pieno controllo del codice. Più avanti mostreremo come questo rendering si possa automatizzare.
Potete generare il template Latte del form con il metodo
Nette\Forms\Blueprint::latte($form), che lo stampa nella pagina del browser. Poi vi basta selezionare il codice con
un clic e copiarlo nel vostro progetto.
{control}
Il modo più semplice di disegnare un form è scrivere nel template:
{control signInForm}
Sull'aspetto del form disegnato si può influire configurando il Renderer e i singoli controlli.
n:name
Collegare la definizione del form nel codice PHP con il codice HTML è estremamente semplice. Basta aggiungere gli attributi
n:name. Tutto qui!
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>Nome utente: <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>
Avete il pieno controllo sull'aspetto del codice HTML risultante. Se usate l'attributo n:name con gli elementi
<select>, <button> o <textarea>, il loro contenuto interno viene riempito
automaticamente. Inoltre il tag <form n:name> crea una variabile locale $form che contiene
l'oggetto del form disegnato, e il tag di chiusura </form> disegna gli eventuali controlli nascosti non ancora
disegnati (lo stesso vale per {form} ... {/form}).
Non dobbiamo però dimenticare di disegnare gli eventuali messaggi di errore. Si tratta degli errori aggiunti ai singoli
controlli con il metodo addError() (disegnati con {inputError}) e degli errori aggiunti direttamente al
form (restituiti da $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>Nome utente: <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>
I controlli più complessi, come RadioList o CheckboxList, si possono disegnare elemento per elemento così:
{foreach $form[gender]->getItems() as $key => $label}
<label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label>
{/foreach}
{label} {input}
Preferite non pensare a quale elemento HTML usare per ogni controllo nel template, se <input>,
<textarea> o altro? La soluzione è il tag universale {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}Nome utente: {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>
Se il form usa un traduttore, le etichette disegnate a partire dalla definizione del form (per esempio
{label username /}) vengono tradotte. Il testo scritto direttamente tra i tag {label} e
{/label} no.
Anche qui i controlli più complessi, come RadioList o CheckboxList, si possono disegnare elemento per elemento:
{foreach $form[gender]->items as $key => $label}
{label gender:$key}{input gender:$key} {$label}{/label}
{/foreach}
Per disegnare solo l'<input> di un controllo Checkbox, usate {input myCheckbox:}. In tal caso
separate sempre gli attributi HTML con una virgola: {input myCheckbox:, class: required}.
{inputError}
Mostra il messaggio di errore di un controllo del form, se esiste. Il messaggio viene di norma racchiuso in un elemento HTML
per lo stile. Si può impedire elegantemente il rendering di un elemento vuoto, quando non c'è alcun messaggio, con
n:ifcontent:
<span class=error n:ifcontent>{inputError $input}</span>
Possiamo verificare la presenza di un errore con il metodo hasErrors() e impostare di conseguenza la classe
dell'elemento genitore:
<div n:class="$form[username]->hasErrors() ? 'error'">
{input username}
{inputError username}
</div>
{form}
I tag {form signInForm}...{/form} sono un'alternativa a
<form n:name="signInForm">...</form>. Separate gli eventuali argomenti dal nome con una virgola:
{form signInForm, class: foo}.
La parola chiave scope posta prima del nome mette il form solo sulla pila (così che
{input}, {label} ecc. si leghino a esso), ma non disegna il tag <form>. È comoda per
disegnare una parte di un form, per esempio in uno snippet. Se un form è già attivo, il nome viene risolto relativamente a esso,
quindi {form scope} sostituisce anche {formContainer}:
{form scope signInForm}
{input username}
{/form}
La parola chiave detached disegna un <form></form> vuoto e collega a
esso ogni controllo tramite l'attributo HTML form. Questo vi permette di collocare un form dentro un altro form, cosa
che l'HTML altrimenti vieta. Il form staccato deve avere un id HTML, che viene generato automaticamente quando gli
date un nome (come outerForm qui sotto):
{form detached outerForm}
...
{/form}
Rendering automatico
Grazie ai tag {input} e {label} possiamo creare facilmente un template generico per qualsiasi form.
Scorrerà e disegnerà tutti i suoi controlli, tranne quelli nascosti, che vengono disegnati automaticamente alla chiusura del
form con il tag </form>. Si aspetta nella variabile $form il nome del form da disegnare.
<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>
I tag di tipo pari autochiudenti {label .../} usati qui mostrano le etichette provenienti dalla definizione del
form nel codice PHP.
Salvate questo template generico, per esempio, nel file basic-form.latte. Per disegnare il form basta includerlo e
passare il nome del form (o l'istanza) al parametro $form:
{include basic-form.latte, form: signInForm}
Se volete modificare l'aspetto di un determinato form durante il rendering, magari disegnando un controllo in modo diverso, il modo più semplice è preparare nel template dei blocchi che si possano poi sovrascrivere. I blocchi possono avere anche nomi dinamici, il che vi permette di inserirvi il nome del controllo disegnato. Per esempio:
...
{label $input /}
{block "input-{$input->name}"}{input $input}{/block}
...
Per un controllo chiamato per esempio username, questo crea il blocco input-username, che si può
facilmente sovrascrivere con il tag {embed}:
{embed basic-form.latte, form: signInForm}
{block input-username}
<span class=important>
{include parent}
</span>
{/block}
{/embed}
In alternativa, l'intero contenuto del template basic-form.latte si può definire come blocco, parametro $form
compreso:
{define basic-form, $form}
<form n:name=$form class=form>
...
</form>
{/define}
Questo ne rende leggermente più semplice la chiamata:
{embed basic-form, signInForm}
...
{/embed}
Il blocco va importato in un solo punto, all'inizio del template di layout:
{import basic-form.latte}
Casi particolari
Se dovete disegnare solo la parte interna del form, senza i tag HTML <form>, per esempio inviando degli
snippet, nascondeteli con l'attributo n:tag-if:
<form n:name=signInForm n:tag-if=false>
<div>
<label n:name=username>Nome utente: <input n:name=username></label>
{inputError username}
</div>
</form>
Il tag {formContainer}, oppure il più recente {form scope}, aiuta a
disegnare i controlli dentro un container del form.
<p>Quali notizie volete ricevere:</p>
{formContainer emailNews}
<ul>
<li>{input sport} {label sport /}</li>
<li>{input science} {label science /}</li>
</ul>
{/formContainer}
Rendering senza Latte
Il modo più semplice di disegnare un form è chiamare:
$form->render();
Sull'aspetto del form disegnato si può influire configurando il Renderer e i singoli controlli.
Rendering manuale
Ogni controllo del form ha metodi che generano il codice HTML del campo e della sua etichetta. Possono restituirlo come stringa oppure come oggetto Nette\Utils\Html:
getControl(): Html|stringrestituisce il codice HTML del controllogetLabel($caption = null): Html|string|nullrestituisce il codice HTML dell'etichetta, se esiste
Questo permette di disegnare il form elemento per elemento:
<?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') ?>
Mentre per alcuni controlli getControl() restituisce un unico elemento HTML (per esempio
<input>, <select> ecc.), per altri restituisce un intero pezzo di codice HTML (CheckboxList,
RadioList). In questi casi potete usare i metodi che generano separatamente i singoli input e le singole etichette:
getControlPart($key = null): Htmlrestituisce il codice HTML di un singolo elementogetLabelPart($key = null): Htmlrestituisce il codice HTML dell'etichetta di un singolo elemento
Questi metodi hanno il prefisso get per motivi storici, ma generate sarebbe più
appropriato, perché a ogni chiamata creano e restituiscono un nuovo elemento Html.
Renderer
È l'oggetto che si occupa di disegnare il form. Si imposta con il metodo $form->setRenderer(). Il controllo
gli viene passato quando viene chiamato il metodo $form->render().
Se non impostiamo un renderer personalizzato, verrà usato il renderer predefinito Nette\Forms\Rendering\DefaultFormRenderer. Esso disegna i controlli del form in una tabella HTML. Il risultato ha questo aspetto:
<table>
<tr class="required">
<th><label class="required" for="frm-name">Nome:</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">Età:</label></th>
<td><input type="text" class="text" name="age" id="frm-age" required value=""></td>
</tr>
<tr>
<th><label>Sesso:</label></th>
...
Se usare una tabella per la struttura del form è discutibile, e molti web designer preferiscono un markup diverso, per esempio
una lista di definizioni. Riconfigureremo quindi DefaultFormRenderer perché disegni il form come elenco. La
configurazione avviene modificando l'array $wrappers. Il primo
indice rappresenta sempre un'area, il secondo un suo attributo. Le singole aree sono mostrate nell'immagine:

Per impostazione predefinita il gruppo controls è racchiuso in <table>, ogni pair
rappresenta una riga della tabella <tr> e la coppia label e control sono le celle
<th> e <td>. Cambieremo ora gli elementi che racchiudono. Collocheremo l'area
controls in un container <dl>, lasceremo l'area pair senza container, metteremo
label in <dt> e infine racchiuderemo control nei tag <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();
Ne risulta il codice HTML seguente:
<dl>
<dt><label class="required" for="frm-name">Nome:</label></dt>
<dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd>
<dt><label class="required" for="frm-age">Età:</label></dt>
<dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd>
<dt><label>Sesso:</label></dt>
...
</dl>
L'array wrappers permette di influire su molti altri attributi:
- aggiungere classi CSS ai singoli tipi di controllo
- distinguere le righe pari e dispari con classi CSS
- distinguere visivamente gli elementi obbligatori da quelli facoltativi
- stabilire se i messaggi di errore vengano mostrati direttamente accanto ai controlli oppure sopra il form
Opzioni
Il comportamento del Renderer si può governare anche impostando delle opzioni sui singoli controlli. Così potete impostare una descrizione che compare accanto al campo:
$form->addText('phone', 'Numero:')
->setOption('description', 'Questo numero resterà nascosto');
Se vogliamo inserirvi contenuto HTML, usiamo la classe Html:
use Nette\Utils\Html;
$form->addText('phone', 'Telefono:')
->setOption('description', Html::el('p')
->setHtml('<a href="...">Condizioni del servizio.</a>')
);
Un elemento Html si può usare anche al posto di un'etichetta:
$form->addCheckbox('conditions', $label).
Raggruppare i controlli
Il Renderer permette di raggruppare i controlli in gruppi visivi (fieldset):
$form->addGroup('Dati personali');
Dopo aver creato un nuovo gruppo, esso diventa attivo e ogni controllo appena aggiunto vi viene aggiunto. Il form si può quindi costruire così:
$form = new Form;
$form->addGroup('Dati personali');
$form->addText('name', 'Il vostro nome:');
$form->addInteger('age', 'La vostra età:');
$form->addEmail('email', 'Email:');
$form->addGroup('Indirizzo di spedizione');
$form->addCheckbox('send', 'Spedisci all\'indirizzo');
$form->addText('street', 'Via:');
$form->addText('city', 'Città:');
$form->addSelect('country', 'Paese:', $countries);
Il renderer disegna prima i gruppi e poi i controlli che non appartengono ad alcun gruppo.
Supporto di Bootstrap
Nella directory degli esempi trovate esempi che mostrano come configurare il Renderer per Twitter Bootstrap 2, Bootstrap 3 e Bootstrap 4.
Attributi HTML
Per impostare attributi HTML qualsiasi sui controlli del form, usate il metodo
setHtmlAttribute(string $name, $value = true):
$form->addInteger('number', 'Numero:')
->setHtmlAttribute('class', 'big-number');
$form->addSelect('rank', 'Ordina per:', ['prezzo', 'nome'])
->setHtmlAttribute('onchange', 'submit()'); // invia il form al cambiamento
// per impostare gli attributi dell'elemento <form> stesso
$form->setHtmlAttribute('id', 'myForm');
Indicare il tipo del controllo:
$form->addText('tel', 'Il vostro telefono:')
->setHtmlType('tel')
->setHtmlAttribute('placeholder', 'Inserite il vostro telefono');
Impostare il tipo e gli altri attributi ha solo scopo visivo. La verifica della correttezza dell'input deve avvenire sul lato server, cosa che garantite scegliendo un controllo appropriato e indicando le regole di validazione.
Per i singoli elementi delle liste di radio button o di checkbox possiamo impostare un attributo HTML con valori diversi per
ciascuno. Notate i due punti dopo style:, che fanno sì che il valore venga scelto in base alla chiave:
$colors = ['r' => 'rosso', 'g' => 'verde', 'b' => 'blu'];
$styles = ['r' => 'background:red', 'g' => 'background:green'];
$form->addCheckboxList('colors', 'Colori:', $colors)
->setHtmlAttribute('style:', $styles);
Disegna:
<label><input type="checkbox" name="colors[]" style="background:red" value="r">rosso</label>
<label><input type="checkbox" name="colors[]" style="background:green" value="g">verde</label>
<label><input type="checkbox" name="colors[]" value="b">blu</label>
Per impostare attributi booleani, come readonly, possiamo usare la notazione con il punto interrogativo:
$form->addCheckboxList('colors', 'Colori:', $colors)
->setHtmlAttribute('readonly?', 'r'); // per più chiavi usate un array, per esempio ['r', 'g']
Disegna:
<label><input type="checkbox" name="colors[]" readonly value="r">rosso</label>
<label><input type="checkbox" name="colors[]" value="g">verde</label>
<label><input type="checkbox" name="colors[]" value="b">blu</label>
Per i select box il metodo setHtmlAttribute() imposta gli attributi dell'elemento <select>. Se
vogliamo impostare gli attributi dei singoli elementi <option>, usiamo il metodo
setOptionAttribute(). Anche qui funzionano le notazioni con i due punti e con il punto interrogativo
appena viste:
$form->addSelect('colors', 'Colori:', $colors)
->setOptionAttribute('style:', $styles);
Disegna:
<select name="colors">
<option value="r" style="background:red">rosso</option>
<option value="g" style="background:green">verde</option>
<option value="b">blu</option>
</select>
Prototipi
Un modo alternativo di impostare gli attributi HTML è modificare il modello da cui viene generato l'elemento HTML. Il modello
è un oggetto Html ed è restituito dal metodo getControlPrototype():
$input = $form->addInteger('number', 'Numero:');
$html = $input->getControlPrototype(); // <input>
$html->class('big-number'); // <input class="big-number">
Anche il modello dell'etichetta, restituito da getLabelPrototype(), si può modificare in questo modo:
$html = $input->getLabelPrototype(); // <label>
$html->class('distinctive'); // <label class="distinctive">
Per i controlli Checkbox, CheckboxList e RadioList potete influire sul modello dell'elemento che racchiude l'intero controllo.
Lo restituisce getContainerPrototype(). Per impostazione predefinita è un elemento “vuoto”, quindi non viene
disegnato nulla, ma dandogli un nome verrà disegnato:
$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>
Nel caso di CheckboxList e RadioList potete influire anche sul modello del separatore tra i singoli elementi, restituito dal
metodo getSeparatorPrototype(). Per impostazione predefinita è l'elemento <br>. Se lo cambiate in
un elemento di tipo pari, racchiuderà i singoli elementi invece di separarli. Potete inoltre influire sul modello dell'elemento
HTML delle etichette dei singoli elementi, restituito da getItemLabelPrototype().
Traduzione
Se sviluppate un'applicazione multilingue, probabilmente avrete bisogno di disegnare il form in versioni linguistiche diverse. Nette Framework definisce a questo scopo un'interfaccia di traduzione: Nette\Localization\Translator. Nette non ha un'implementazione predefinita; potete scegliere tra diverse soluzioni già pronte disponibili su Componette, secondo le vostre esigenze. La loro documentazione spiega come configurare il traduttore.
I form supportano la stampa dei testi tramite il traduttore. Lo passiamo con il metodo setTranslator():
$form->setTranslator($translator);
Da questo momento verranno tradotte nella lingua di destinazione non solo tutte le etichette, ma anche tutti i messaggi di errore, gli elementi dei select box e i placeholder dei campi.
È possibile impostare un traduttore diverso per i singoli controlli del form oppure disattivare completamente la traduzione
impostando il valore a null:
$form->addSelect('carModel', 'Modello:', $cars)
->setTranslator(null);
Per le regole di validazione al traduttore vengono passati anche i parametri specifici. Per esempio, per la regola:
$form->addPassword('password', 'Password:')
->addRule($form::MinLength, 'La password deve essere lunga almeno %d caratteri', 8);
il traduttore viene chiamato con questi parametri:
$translator->translate('La password deve essere lunga almeno %d caratteri', 8);
e può quindi scegliere la forma plurale corretta della parola caratteri in base al numero.
Evento onRender
Poco prima che il form venga disegnato, possiamo far eseguire il nostro codice. Questo codice può, per esempio, aggiungere
classi HTML ai controlli del form per una corretta visualizzazione. Aggiungiamo il codice all'array onRender:
$form->onRender[] = function ($form) {
BootstrapCSS::initialize($form);
};