Renderizado de formularios
El aspecto de los formularios puede ser muy variado. En la práctica podemos encontrarnos con dos extremos. Por un lado está
la necesidad de renderizar en una aplicación un montón de formularios visualmente idénticos, y agradecemos el renderizado
sencillo sin plantilla con $form->render(). Es el caso típico de las interfaces de administración.
Por otro lado están los formularios variados, en los que cada uno es único. Su aspecto se describe mejor con HTML en la plantilla del formulario. Y, por supuesto, además de estos dos extremos nos encontramos con muchos formularios que quedan en algún punto intermedio.
Renderizado con Latte
El sistema de plantillas Latte simplifica notablemente el renderizado de los formularios y de sus elementos. Primero mostraremos cómo renderizar un formulario a mano, elemento por elemento, para tener pleno control sobre el código. Más adelante veremos cómo se puede automatizar ese renderizado.
La plantilla Latte del formulario se puede generar con el método
Nette\Forms\Blueprint::latte($form), que la imprime en la página del navegador. Después basta con seleccionar el
código con un clic y copiarlo a su proyecto.
{control}
La forma más sencilla de renderizar un formulario es escribir en la plantilla:
{control signInForm}
El aspecto del formulario renderizado se puede influir configurando el Renderer y los distintos elementos.
n:name
Enlazar la definición del formulario en el código PHP con el código HTML es extremadamente fácil. Basta con añadir los
atributos n:name. ¡Así de sencillo!
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>
Tiene pleno control sobre el aspecto del código HTML resultante. Si usa el atributo n:name con los elementos
<select>, <button> o <textarea>, su contenido interno se rellena
automáticamente. Además, la etiqueta <form n:name> crea una variable local $form con el objeto
del formulario renderizado, y la etiqueta de cierre </form> renderiza todos los elementos ocultos que no se
hayan renderizado (lo mismo vale para {form} ... {/form}).
Pero no debemos olvidarnos de renderizar los posibles mensajes de error. Tanto los añadidos a los distintos elementos con el
método addError() (que se renderizan con {inputError}) como los añadidos directamente al formulario
(que devuelve $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>
Los elementos de formulario más complejos, como RadioList o CheckboxList, se pueden renderizar elemento a elemento así:
{foreach $form[gender]->getItems() as $key => $label}
<label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label>
{/foreach}
{label} {input}
¿Prefiere no pensar en la plantilla qué elemento HTML usar para cada elemento del formulario, si <input>,
<textarea>, etc.? La solución es la etiqueta universal {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 el formulario usa un traductor, las etiquetas renderizadas a partir de la definición del formulario (p. ej.
{label username /}) se traducen. El texto escrito directamente entre las etiquetas {label} y
{/label} no.
También aquí, los elementos de formulario más complejos, como RadioList o CheckboxList, se pueden renderizar elemento a elemento:
{foreach $form[gender]->items as $key => $label}
{label gender:$key}{input gender:$key} {$label}{/label}
{/foreach}
Para renderizar solo el <input> de un elemento Checkbox, use {input myCheckbox:}. En ese caso,
separe siempre los atributos HTML con una coma: {input myCheckbox:, class: required}.
{inputError}
Muestra el mensaje de error de un elemento del formulario, si existe. El mensaje se suele envolver en un elemento HTML para
darle estilo. Evitar que se renderice un elemento vacío cuando no hay mensaje se consigue elegantemente con
n:ifcontent:
<span class=error n:ifcontent>{inputError $input}</span>
La presencia de un error se puede comprobar con el método hasErrors() y establecer en consecuencia la clase del
elemento padre:
<div n:class="$form[username]->hasErrors() ? 'error'">
{input username}
{inputError username}
</div>
{form}
Las etiquetas {form signInForm}...{/form} son una alternativa a
<form n:name="signInForm">...</form>. Los posibles argumentos se separan del nombre con una coma:
{form signInForm, class: foo}.
La palabra clave scope colocada delante del nombre solo mete el formulario en la pila (para
que {input}, {label}, etc. se enlacen con él), pero no renderiza la etiqueta <form>.
Resulta práctica para renderizar una parte del formulario, p. ej. en un snippet. Si ya hay un formulario activo, el nombre se
resuelve de forma relativa a él, así que {form scope} sustituye también a {formContainer}:
{form scope signInForm}
{input username}
{/form}
La palabra clave detached renderiza un <form></form> vacío y enlaza
con él todos los elementos mediante el atributo HTML form. Eso le permite colocar un formulario dentro de otro
formulario, cosa que HTML normalmente prohíbe. El formulario detached debe tener un id HTML, que se genera
automáticamente cuando le da un nombre (como outerForm más abajo):
{form detached outerForm}
...
{/form}
Renderizado automático
Gracias a las etiquetas {input} y {label} podemos crear fácilmente una plantilla genérica para
cualquier formulario. Recorrerá y renderizará todos sus elementos, salvo los ocultos, que se renderizan automáticamente al
cerrar el formulario con la etiqueta </form>. Espera el nombre del formulario a renderizar en 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>
Las etiquetas pareadas autocerradas {label .../} que se usan aquí muestran las etiquetas que provienen de la
definición del formulario en el código PHP.
Guarde esta plantilla genérica, por ejemplo, en el archivo basic-form.latte. Para renderizar el formulario basta
con incluirla y pasar el nombre del formulario (o su instancia) al parámetro $form:
{include basic-form.latte, form: signInForm}
Si quiere modificar el aspecto de un formulario concreto al renderizarlo, y quizá renderizar un elemento de otra manera, lo más fácil es preparar en la plantilla bloques que se puedan sobrescribir después. Los bloques también pueden tener nombres dinámicos, lo que le permite insertar el nombre del elemento renderizado. Por ejemplo:
...
{label $input /}
{block "input-{$input->name}"}{input $input}{/block}
...
Para un elemento llamado, p. ej., username, se crea así el bloque input-username, que se puede
sobrescribir fácilmente con la etiqueta {embed}:
{embed basic-form.latte, form: signInForm}
{block input-username}
<span class=important>
{include parent}
</span>
{/block}
{/embed}
Alternativamente, todo el contenido de la plantilla basic-form.latte se puede definir como un bloque, incluido el parámetro
$form:
{define basic-form, $form}
<form n:name=$form class=form>
...
</form>
{/define}
Eso hace su llamada un poco más simple:
{embed basic-form, signInForm}
...
{/embed}
El bloque solo hace falta importarlo en un sitio, al principio de la plantilla del layout:
{import basic-form.latte}
Casos especiales
Si necesita renderizar solo la parte interna del formulario sin las etiquetas HTML <form>, por ejemplo al
enviar snippets, ocúltelas con el atributo 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 el renderizado de los elementos que hay dentro de un contenedor del formulario ayuda la etiqueta
{formContainer}, o la más reciente {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}
Renderizado sin Latte
La forma más fácil de renderizar un formulario es llamar a:
$form->render();
El aspecto del formulario renderizado se puede influir configurando el Renderer y los distintos elementos.
Renderizado manual
Cada elemento del formulario tiene métodos que generan el código HTML del campo y de su etiqueta. Pueden devolverlo como cadena o como objeto Nette\Utils\Html:
getControl(): Html|stringdevuelve el código HTML del elementogetLabel($caption = null): Html|string|nulldevuelve el código HTML de la etiqueta, si existe
Eso permite renderizar el formulario elemento por 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') ?>
Mientras que en algunos elementos getControl() devuelve un único elemento HTML (p. ej.
<input>, <select>, etc.), en otros devuelve un fragmento completo de código HTML
(CheckboxList, RadioList). En esos casos puede usar los métodos que generan por separado los distintos inputs y etiquetas de
cada ítem:
getControlPart($key = null): Htmldevuelve el código HTML de un solo ítemgetLabelPart($key = null): Htmldevuelve el código HTML de la etiqueta de un solo ítem
Estos métodos llevan el prefijo get por motivos históricos, pero generate sería más
adecuado, ya que crean y devuelven un elemento Html nuevo en cada llamada.
Renderer
Es un objeto que se encarga de renderizar el formulario. Se establece con el método $form->setRenderer(). Se
le pasa el control cuando se llama al método $form->render().
Si no establecemos un renderer propio, se usará el renderer predeterminado Nette\Forms\Rendering\DefaultFormRenderer. Este renderiza los elementos del formulario en una tabla HTML. La salida tiene este aspecto:
<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>
...
Si usar o no una tabla para la estructura del formulario es discutible, y muchos diseñadores web prefieren otro marcado, por
ejemplo una lista de definiciones. Por eso reconfiguraremos DefaultFormRenderer para que renderice el formulario como
una lista. La configuración se hace editando el array $wrappers. El
primer índice representa siempre un área y el segundo su atributo. Las distintas áreas se ven en la imagen:

De forma predeterminada, el grupo controls está envuelto en <table>, cada pair
representa una fila de la tabla <tr> y el par label y control son las celdas
<th> y <td>. Ahora cambiaremos los elementos envolventes. Colocaremos el área
controls en un contenedor <dl>, dejaremos el área pair sin contenedor, pondremos la
label en <dt> y, por último, envolveremos el control con las etiquetas
<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();
El resultado es el siguiente código HTML:
<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>
El array wrappers permite influir en muchos otros atributos:
- añadir clases CSS a los distintos tipos de elementos del formulario
- distinguir con clases CSS las filas pares e impares
- distinguir visualmente los ítems obligatorios de los opcionales
- determinar si los mensajes de error se muestran directamente junto a los elementos o encima del formulario
Opciones
El comportamiento del Renderer también se puede controlar estableciendo opciones en los distintos elementos del formulario. Así puede establecer una descripción que aparece junto al campo de entrada:
$form->addText('phone', 'Number:')
->setOption('description', 'This number will remain hidden');
Si queremos poner ahí contenido HTML, usamos la clase Html:
use Nette\Utils\Html;
$form->addText('phone', 'Phone:')
->setOption('description', Html::el('p')
->setHtml('<a href="...">Terms of service.</a>')
);
Un elemento Html se puede usar también en lugar de la etiqueta:
$form->addCheckbox('conditions', $label).
Agrupar elementos
El Renderer permite agrupar los elementos en grupos visuales (fieldsets):
$form->addGroup('Personal data');
Tras crear un grupo nuevo, este pasa a estar activo, y cada elemento recién añadido se añade también a él. Así que el formulario se puede construir de esta manera:
$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);
El renderer dibuja primero los grupos y después los elementos que no pertenecen a ningún grupo.
Soporte de Bootstrap
En el directorio de ejemplos encontrará ejemplos de cómo configurar el Renderer para Twitter Bootstrap 2, Bootstrap 3 y Bootstrap 4.
Atributos HTML
Para establecer cualquier atributo HTML de los elementos del formulario, use el método
setHtmlAttribute(string $name, $value = true):
$form->addInteger('number', 'Number:')
->setHtmlAttribute('class', 'big-number');
$form->addSelect('rank', 'Order by:', ['price', 'name'])
->setHtmlAttribute('onchange', 'submit()'); // envía el formulario al cambiar
// Para establecer los atributos del propio elemento <form>
$form->setHtmlAttribute('id', 'myForm');
Especificar el tipo del elemento:
$form->addText('tel', 'Your telephone:')
->setHtmlType('tel')
->setHtmlAttribute('placeholder', 'Please, fill in your telephone');
Establecer el tipo y otros atributos solo tiene fines visuales. La verificación de que la entrada es correcta debe ocurrir en el lado del servidor, algo que consigue eligiendo un elemento de formulario adecuado e indicando reglas de validación.
En los distintos ítems de las listas de radio o de checkbox podemos establecer un atributo HTML con valores diferentes para
cada uno. Fíjese en los dos puntos tras style:, que hacen que el valor se seleccione según la clave:
$colors = ['r' => 'red', 'g' => 'green', 'b' => 'blue'];
$styles = ['r' => 'background:red', 'g' => 'background:green'];
$form->addCheckboxList('colors', 'Colors:', $colors)
->setHtmlAttribute('style:', $styles);
Renderiza:
<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>
Para establecer atributos booleanos, como readonly, podemos usar la notación con signo de interrogación:
$form->addCheckboxList('colors', 'Colors:', $colors)
->setHtmlAttribute('readonly?', 'r'); // para varias claves use un array, p. ej. ['r', 'g']
Renderiza:
<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>
En las listas desplegables, el método setHtmlAttribute() establece los atributos del elemento
<select>. Si queremos establecer los atributos de los distintos elementos <option>, usamos
el método setOptionAttribute(). Las notaciones con dos puntos y con signo de interrogación mencionadas arriba
también funcionan:
$form->addSelect('colors', 'Colors:', $colors)
->setOptionAttribute('style:', $styles);
Renderiza:
<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>
Prototipos
Otra forma de establecer atributos HTML es modificar la plantilla a partir de la cual se genera el elemento HTML. La plantilla
es un objeto Html y la devuelve el método getControlPrototype():
$input = $form->addInteger('number', 'Number:');
$html = $input->getControlPrototype(); // <input>
$html->class('big-number'); // <input class="big-number">
De esta manera se puede modificar también la plantilla de la etiqueta, que devuelve getLabelPrototype():
$html = $input->getLabelPrototype(); // <label>
$html->class('distinctive'); // <label class="distinctive">
En los elementos Checkbox, CheckboxList y RadioList puede influir en la plantilla del elemento que envuelve todo el control. La
devuelve getContainerPrototype(). De forma predeterminada es un elemento “vacío”, así que no se renderiza nada,
pero si le da un nombre se renderizará:
$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>
En el caso de CheckboxList y RadioList puede influir también en la plantilla del separador de los distintos ítems, que
devuelve el método getSeparatorPrototype(). De forma predeterminada es el elemento <br>. Si lo
cambia por un elemento pareado, envolverá los distintos ítems en lugar de separarlos. Además, puede influir en la plantilla del
elemento HTML de las etiquetas de los distintos ítems, que devuelve getItemLabelPrototype().
Traducción
Si desarrolla una aplicación multilingüe, probablemente necesitará renderizar el formulario en distintas versiones de idioma. Nette Framework define para ello una interfaz de traducción: Nette\Localization\Translator. Nette no trae ninguna implementación predeterminada; puede elegir entre varias soluciones ya hechas que encontrará en Componette según sus necesidades. En su documentación aprenderá cómo configurar el traductor.
Los formularios soportan la salida de textos a través del traductor. Se lo pasamos con el método
setTranslator():
$form->setTranslator($translator);
A partir de ese momento se traducirán al idioma de destino no solo todas las etiquetas, sino también todos los mensajes de error, los ítems de las listas desplegables y los placeholders de los campos.
Es posible establecer un traductor distinto para elementos concretos del formulario, o desactivar la traducción por completo
poniendo el valor null:
$form->addSelect('carModel', 'Model:', $cars)
->setTranslator(null);
En las reglas de validación se le pasan al traductor además parámetros concretos. Por ejemplo, para la regla:
$form->addPassword('password', 'Password:')
->addRule($form::MinLength, 'Password must be at least %d characters long', 8);
el traductor se llama con estos parámetros:
$translator->translate('Password must be at least %d characters long', 8);
y así puede elegir la forma plural correcta de la palabra characters según el número.
Evento onRender
Justo antes de renderizar el formulario podemos hacer que se ejecute nuestro código. Ese código puede, por ejemplo, añadir
clases HTML a los elementos del formulario para que se muestren correctamente. Añadimos el código al array
onRender:
$form->onRender[] = function ($form) {
BootstrapCSS::initialize($form);
};