Renderizzazione dei Form

L'aspetto dei form può essere molto vario. In pratica, possiamo incontrare due estremi. Da un lato, c'è la necessità di renderizzare nell'applicazione una serie di form che sono visivamente simili come due gocce d'acqua, e apprezzeremo la facile renderizzazione senza template tramite $form->render(). Questo è solitamente il caso delle interfacce di amministrazione.

Dall'altro lato, ci sono form diversi, dove vale la regola: ogni pezzo è un originale. La loro forma è meglio descritta dal linguaggio HTML nel template del form. E naturalmente, oltre ai due estremi menzionati, incontreremo molti form che si trovano da qualche parte nel mezzo.

Renderizzazione tramite Latte

Il sistema di templating Latte facilita enormemente la renderizzazione dei form e dei loro elementi. Prima mostreremo come renderizzare i form manualmente, elemento per elemento, ottenendo così il pieno controllo sul codice. Successivamente mostreremo come tale renderizzazione possa essere automatizzata.

Puoi farti generare il design del template Latte del form tramite il metodo Nette\Forms\Blueprint::latte($form), che lo stamperà nella pagina del browser. Basta quindi cliccare per selezionare il codice e copiarlo nel progetto.

{control}

Il modo più semplice per renderizzare un form è scrivere nel template:

{control signInForm}

È possibile influenzare l'aspetto di un form così renderizzato configurando il Renderer e i singoli elementi.

n:name

La definizione del form nel codice PHP può essere collegata molto facilmente al codice HTML. Basta aggiungere gli attributi n:name. È così facile!

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>

Avete il pieno controllo sull'aspetto del codice HTML risultante. Se utilizzate l'attributo n:name sugli elementi <select>, <button> o <textarea>, il loro contenuto interno verrà completato automaticamente. Il tag <form n:name> crea inoltre una variabile locale $form con l'oggetto del form disegnato e il tag di chiusura </form> renderizza tutti gli elementi nascosti non renderizzati (lo stesso vale anche per {form} ... {/form}).

Non dobbiamo però dimenticare di renderizzare eventuali messaggi di errore. Sia quelli aggiunti ai singoli elementi con il metodo addError() (tramite {inputError}), sia quelli 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>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>

Elementi del form più complessi, come RadioList o CheckboxList, possono essere renderizzati in questo modo per singoli elementi:

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

{label} {input}

Non vuoi pensare per ogni elemento quale elemento HTML usare nel template, se <input>, <textarea>, ecc.? 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}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>

Se il form utilizza un traduttore, il testo all'interno dei tag {label} verrà tradotto.

Anche in questo caso, elementi del form più complessi, come RadioList o CheckboxList, possono essere renderizzati per singoli elementi:

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

Per renderizzare solo l'<input> nell'elemento Checkbox, usa {input myCheckbox:}. Gli attributi HTML in questo caso vanno sempre separati da virgola {input myCheckbox:, class: required}.

{inputError}

Stampa il messaggio di errore per l'elemento del form, se ne ha uno. Il messaggio viene solitamente racchiuso in un elemento HTML per lo styling. Evitare la renderizzazione di un elemento vuoto, se il messaggio non c'è, è possibile elegantemente tramite n:ifcontent:

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

Possiamo verificare la presenza di un errore con il metodo hasErrors() e in base a ciò impostare una classe sull'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>.

Renderizzazione automatica

Grazie ai tag {input} e {label} possiamo facilmente creare un template generico per qualsiasi form. Itererà e renderizzerà gradualmente tutti i suoi elementi, ad eccezione degli elementi nascosti, che verranno renderizzati automaticamente alla chiusura del form con il tag </form>. Il nome del form da renderizzare sarà atteso nella variabile $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>

I tag di coppia auto-chiudenti {label .../} utilizzati visualizzano le etichette provenienti dalla definizione del form nel codice PHP.

Salvate questo template generico ad esempio nel file basic-form.latte e per renderizzare il form basta includerlo e passare il nome (o l'istanza) del form al parametro $form:

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

Se durante la renderizzazione di un particolare form voleste intervenire sulla sua forma e ad esempio renderizzare un elemento diversamente, allora il modo più semplice è preparare nel template dei blocchi che potranno essere successivamente sovrascritti. I blocchi possono avere anche nomi dinamici, quindi è possibile inserirvi anche il nome dell'elemento renderizzato. Ad esempio:

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

Per l'elemento ad esempio username verrà così creato il blocco input-username, che può essere facilmente sovrascritto utilizzando 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 può essere definito come blocco, incluso il parametro $form:

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

Grazie a ciò, la sua chiamata sarà leggermente più semplice:

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

Il blocco basta importarlo in un unico punto, all'inizio del template di layout:

{import basic-form.latte}

Casi speciali

Se hai bisogno di renderizzare solo la parte interna del form senza i tag HTML <form>, ad esempio quando invii snippet, nascondili usando l'attributo 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>

Con la renderizzazione degli elementi all'interno del contenitore del form aiuta il tag {formContainer}.

<p>Quali notizie desideri ricevere:</p>

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

Renderizzazione senza Latte

Il modo più semplice per renderizzare un form è chiamare:

$form->render();

È possibile influenzare l'aspetto di un form così renderizzato configurando il Renderer e i singoli elementi.

Renderizzazione manuale

Ogni elemento del form dispone di metodi che generano il codice HTML del campo del form e dell'etichetta. Possono restituirlo sia come stringa che come oggetto Nette\Utils\Html:

  • getControl(): Html|string restituisce il codice HTML dell'elemento
  • getLabel($caption = null): Html|string|null restituisce il codice HTML dell'etichetta, se esiste

Il form può quindi essere renderizzato per singoli elementi:

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

Mentre per alcuni elementi getControl() restituisce un singolo elemento HTML (es. <input>, <select>, ecc.), per altri restituisce un intero pezzo di codice HTML (CheckboxList, RadioList). In tal caso, potete utilizzare metodi che generano singoli input ed etichette, per ogni elemento separatamente:

  • getControlPart($key = null): ?Html restituisce il codice HTML di un singolo elemento
  • getLabelPart($key = null): ?Html restituisce il codice HTML dell'etichetta di un singolo elemento

Questi metodi hanno per motivi storici il prefisso get, ma sarebbe stato meglio generate, perché ad ogni chiamata creano e restituiscono un nuovo elemento Html.

Renderer

È un oggetto che si occupa della renderizzazione del form. Può essere impostato con il metodo $form->setRenderer. Gli viene passato il controllo quando viene chiamato il metodo $form->render().

Se non impostiamo un renderer personalizzato, verrà utilizzato il renderer predefinito Nette\Forms\Rendering\DefaultFormRenderer. Questo renderizza gli elementi del form sotto forma di tabella HTML. L'output è simile a questo:

<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 utilizzare o meno una tabella per la struttura del form è discutibile e molti web designer preferiscono un markup diverso. Ad esempio, una lista di definizioni. Riconfigureremo quindi DefaultFormRenderer in modo che renderizzi il form sotto forma di lista. La configurazione viene eseguita modificando l'array $wrappers. Il primo indice rappresenta sempre l'area e il secondo il suo attributo. Le singole aree sono illustrate nell'immagine:

Standardmente il gruppo di elementi controls è avvolto da una tabella <table>, ogni pair rappresenta una riga della tabella <tr> e la coppia label e control sono le celle <th> e <td>. Ora cambieremo gli elementi contenitori. L'area controls la inseriremo nel contenitore <dl>, l'area pair la lasceremo senza contenitore, label la inseriremo in <dt> e infine control lo avvolgeremo con i 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();

Il risultato è questo codice HTML:

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

Nell'array wrappers è possibile influenzare tutta una serie di altri attributi:

  • aggiungere classi CSS a singoli tipi di elementi del form
  • distinguere con classi CSS le righe pari e dispari
  • distinguere visivamente gli elementi obbligatori e facoltativi
  • determinare se i messaggi di errore verranno visualizzati direttamente accanto agli elementi o sopra il form

Options

Il comportamento del Renderer può essere controllato anche impostando options sui singoli elementi del form. In questo modo è possibile impostare una descrizione che verrà visualizzata accanto al campo di input:

$form->addText('phone', 'Numero:')
	->setOption('description', 'Questo numero rimarrà nascosto');

Se vogliamo inserirvi contenuto HTML, utilizziamo la classe Html

use Nette\Utils\Html;

$form->addText('phone', 'Numero:')
	->setOption('description', Html::el('p')
		->setHtml('<a href="...">Condizioni di conservazione del tuo numero</a>')
	);

L'elemento Html può essere utilizzato anche al posto dell'etichetta: $form->addCheckbox('conditions', $label).

Raggruppamento degli elementi

Il Renderer consente di raggruppare gli elementi in gruppi visivi (fieldset):

$form->addGroup('Dati personali');

Dopo aver creato un nuovo gruppo, questo diventa attivo e ogni elemento appena aggiunto viene aggiunto anche ad esso. Quindi il form può essere costruito in questo modo:

$form = new Form;
$form->addGroup('Dati personali');
$form->addText('name', 'Il tuo nome:');
$form->addInteger('age', 'La tua 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 renderizza prima i gruppi e solo dopo gli elementi che non appartengono a nessun gruppo.

Supporto per Bootstrap

Negli esempi troverete esempi su come configurare il Renderer per Twitter Bootstrap 2, Bootstrap 3 e Bootstrap 4

Attributi HTML

Per impostare attributi HTML arbitrari degli elementi del form, utilizziamo 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 alla modifica


// Per impostare gli attributi del <form> stesso
$form->setHtmlAttribute('id', 'myForm');

Specifica del tipo di elemento:

$form->addText('tel', 'Il tuo telefono:')
	->setHtmlType('tel')
	->setHtmlAttribute('placeholder', 'scrivi il telefono');

L'impostazione del tipo e di altri attributi serve solo a scopi visivi. La verifica della correttezza degli input deve avvenire lato server, cosa che si garantisce scegliendo l'elemento del form appropriato e indicando le regole di validazione.

Alle singole voci nelle liste radio o checkbox possiamo impostare un attributo HTML con valori diversi per ciascuna di esse. Notate i due punti dopo style:, che assicurano la scelta del valore 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);

Stampa:

<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 logici, come readonly, possiamo usare la scrittura con il punto interrogativo:

$form->addCheckboxList('colors', 'Colori:', $colors)
	->setHtmlAttribute('readonly?', 'r'); // per più chiavi usa un array, es. ['r', 'g']

Stampa:

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

Nel caso dei selectbox, il metodo setHtmlAttribute() imposta gli attributi dell'elemento <select>. Se vogliamo impostare attributi ai singoli <option>, usiamo il metodo setOptionAttribute(). Funzionano anche le scritture con i due punti e il punto interrogativo indicate sopra:

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

Stampa:

<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 per impostare gli attributi HTML consiste nel modificare il prototipo da cui viene generato l'elemento HTML. Il prototipo è un oggetto Html e viene restituito dal metodo getControlPrototype():

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

In questo modo è possibile modificare anche il prototipo dell'etichetta, restituito da getLabelPrototype():

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

Per gli elementi Checkbox, CheckboxList e RadioList è possibile influenzare il prototipo dell'elemento che avvolge l'intero elemento. Viene restituito da getContainerPrototype(). Nello stato predefinito è un elemento “vuoto”, quindi non viene renderizzato nulla, ma impostandogli un nome, verrà renderizzato:

$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 è possibile influenzare anche il prototipo del separatore delle singole voci, restituito dal metodo getSeparatorPrototype(). Nello stato predefinito è l'elemento <br>. Se lo si modifica in un elemento di coppia, avvolgerà le singole voci invece di separarle. Inoltre, è possibile influenzare il prototipo dell'elemento HTML dell'etichetta delle singole voci, restituito da getItemLabelPrototype().

Traduzione

Se programmi un'applicazione multilingue, probabilmente avrai bisogno di renderizzare il form in diverse versioni linguistiche. Nette Framework definisce a tale scopo un'interfaccia per la traduzione Nette\Localization\Translator. In Nette non c'è un'implementazione predefinita, puoi scegliere in base alle tue esigenze tra diverse soluzioni pronte, che trovi su Componette. Nella loro documentazione scoprirai come configurare il traduttore.

I form supportano la stampa di testi tramite il traduttore. Glielo passiamo tramite il metodo setTranslator():

$form->setTranslator($translator);

Da questo momento, non solo tutte le etichette, ma anche tutti i messaggi di errore o le voci dei select box verranno tradotti in un'altra lingua.

Per i singoli elementi del form è possibile impostare un traduttore diverso o disattivare completamente la traduzione con il valore null:

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

Alle regole di validazione vengono passati al traduttore anche parametri specifici, ad esempio per la regola:

$form->addPassword('password', 'Password:')
	->addRule($form::MinLength, 'La password deve avere almeno %d caratteri', 8);

viene chiamato il traduttore con questi parametri:

$translator->translate('La password deve avere almeno %d caratteri', 8);

e quindi può scegliere la forma plurale corretta della parola caratteri in base al numero.

Evento onRender

Poco prima che il form venga renderizzato, possiamo far chiamare il nostro codice. Questo può, ad esempio, aggiungere classi HTML agli elementi del form per una corretta visualizzazione. Aggiungiamo il codice all'array onRender:

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