Renderizado de formularios
La apariencia de los formularios puede ser muy diversa. En la práctica, podemos encontrar dos extremos. Por un lado, está la
necesidad de renderizar en la aplicación una serie de formularios que son visualmente similares como dos gotas de agua, y
apreciaremos el fácil renderizado sin plantilla usando $form->render()
. Este suele ser el caso de las interfaces
de administración.
Por otro lado, están los formularios diversos donde aplica: cada pieza es original. Su forma se describe mejor usando el lenguaje HTML en la plantilla del formulario. Y, por supuesto, además de ambos extremos mencionados, encontraremos muchos formularios que se mueven en algún punto intermedio.
Renderizado mediante Latte
El sistema de plantillas Latte facilita fundamentalmente el renderizado de formularios y sus elementos. Primero mostraremos cómo renderizar formularios manualmente, elemento por elemento, y así obtener control total sobre el código. Más adelante mostraremos cómo se puede automatizar dicho renderizado.
Puede hacer que el diseño de la plantilla Latte del formulario se genere usando el método
Nette\Forms\Blueprint::latte($form)
, que lo imprimirá en la página del navegador. Luego, simplemente haga clic para
seleccionar el código y cópielo en su proyecto.
{control}
La forma más sencilla de renderizar un formulario es escribir en la plantilla:
{control signInForm}
Se puede influir en la apariencia del formulario renderizado de esta manera configurando el Renderer y los elementos individuales.
n:name
La definición del formulario en el código PHP se puede vincular muy fácilmente con el código HTML. Simplemente agregue los
atributos n:name
. ¡Así de fácil!
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 control total sobre la forma del código HTML resultante. Si usa el atributo n:name
en los elementos
<select>
, <button>
o <textarea>
, su contenido interno se completará
automáticamente. La etiqueta <form n:name>
además crea una variable local $form
con el objeto
del formulario que se está renderizando y el cierre </form>
renderiza todos los elementos ocultos no
renderizados (lo mismo aplica a {form} ... {/form}
).
Sin embargo, no debemos olvidar renderizar los posibles mensajes de error. Tanto los que se agregaron a elementos individuales
con el método addError()
(usando {inputError}
), como los agregados directamente al formulario
(devueltos por $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 así por elementos individuales:
{foreach $form[gender]->getItems() as $key => $label}
<label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label>
{/foreach}
{label}
{input}
¿No quiere pensar para cada elemento qué elemento HTML usar en la plantilla, 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 utiliza un traductor, el texto dentro de las etiquetas {label}
será traducido.
Incluso en este caso, los elementos de formulario más complejos, como RadioList o CheckboxList, se pueden renderizar por elementos individuales:
{foreach $form[gender]->items as $key => $label}
{label gender:$key}{input gender:$key} {$label}{/label}
{/foreach}
Para renderizar solo el <input>
en el elemento Checkbox, use {input myCheckbox:}
. Los atributos
HTML en este caso siempre se separan con coma {input myCheckbox:, class: required}
.
{inputError}
Muestra el mensaje de error para un elemento de formulario, si tiene alguno. El mensaje generalmente se envuelve en un elemento
HTML para estilizarlo. Evitar renderizar un elemento vacío si no hay mensaje se puede hacer elegantemente usando
n:ifcontent
:
<span class=error n:ifcontent>{inputError $input}</span>
Podemos verificar la presencia de un error con el método hasErrors()
y establecer la clase del elemento padre en
consecuencia:
<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>
.
Renderizado automático
Gracias a las etiquetas {input}
y {label}
, podemos crear fácilmente una plantilla genérica para
cualquier formulario. Iterará y renderizará gradualmente todos sus elementos, excepto los elementos ocultos, que se
renderizarán automáticamente al cerrar el formulario con la etiqueta </form>
. Esperará 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 pares autocerradas {label .../}
utilizadas muestran las etiquetas provenientes de la definición del
formulario en el código PHP.
Guarde esta plantilla genérica, por ejemplo, en el archivo basic-form.latte
y para renderizar el formulario,
simplemente inclúyala y pase el nombre (o instancia) del formulario al parámetro $form
:
{include basic-form.latte, form: signInForm}
Si al renderizar un formulario específico quisiera intervenir en su forma y, por ejemplo, renderizar un elemento de manera diferente, entonces la forma más sencilla es preparar bloques en la plantilla que luego se puedan sobrescribir. Los bloques también pueden tener nombres dinámicos, por lo que también se puede insertar el nombre del elemento que se está renderizando. Por ejemplo:
...
{label $input /}
{block "input-{$input->name}"}{input $input}{/block}
...
Para un elemento, por ejemplo, username
, se creará el bloque input-username
, que se puede
sobrescribir fácilmente usando 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}
Gracias a esto, su llamada será ligeramente más simple:
{embed basic-form, signInForm}
...
{/embed}
El bloque solo necesita importarse en un solo lugar, al principio de la plantilla de 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 usando 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>
La etiqueta {formContainer}
ayuda a renderizar elementos dentro de un contenedor de formulario.
<p>Qué noticias desea recibir:</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 sencilla de renderizar un formulario es llamar a:
$form->render();
Se puede influir en la apariencia del formulario renderizado de esta manera configurando el Renderer y los elementos individuales.
Renderizado manual
Cada elemento de formulario dispone de métodos que generan el código HTML del campo de formulario y la etiqueta. Pueden devolverlo como una cadena o como un objeto Nette\Utils\Html:
getControl(): Html|string
devuelve el código HTML del elementogetLabel($caption = null): Html|string|null
devuelve el código HTML de la etiqueta, si existe
Así, el formulario se puede renderizar elemento por elemento:
<?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') ?>
Mientras que para algunos elementos getControl()
devuelve un único elemento HTML (por ejemplo,
<input>
, <select>
, etc.), para otros devuelve una pieza completa de código HTML
(CheckboxList, RadioList). En tal caso, puede utilizar métodos que generan inputs y etiquetas individuales, para cada elemento
por separado:
getControlPart($key = null): ?Html
devuelve el código HTML de un elementogetLabelPart($key = null): ?Html
devuelve el código HTML de la etiqueta de un elemento
Estos métodos tienen el prefijo get
por razones históricas, pero generate
sería
mejor, porque en cada llamada crean y devuelven un nuevo elemento Html
.
Renderer
Es un objeto que se encarga de renderizar el formulario. Se puede establecer mediante el método
$form->setRenderer
. Se le pasa el control cuando se llama al método $form->render()
.
Si no establecemos nuestro propio renderer, se utilizará el renderer predeterminado Nette\Forms\Rendering\DefaultFormRenderer. Este renderiza los elementos del formulario en forma de tabla HTML. La salida se ve así:
<table>
<tr class="required">
<th><label class="required" for="frm-name">Nombre:</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">Edad:</label></th>
<td><input type="text" class="text" name="age" id="frm-age" required value=""></td>
</tr>
<tr>
<th><label>Género:</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 definición. Por lo tanto, reconfiguraremos DefaultFormRenderer
para que renderice el
formulario en forma de lista. La configuración se realiza editando el array $wrappers. El
primer índice siempre representa el área y el segundo su atributo. Las áreas individuales se muestran en la imagen:

Por defecto, el grupo de elementos controls
está envuelto en una tabla <table>
, cada
pair
representa una fila de la tabla <tr>
y el par label
y control
son
celdas <th>
y <td>
. Ahora cambiaremos los elementos envolventes. El área
controls
la insertaremos en un contenedor <dl>
, el área pair
la dejaremos sin
contenedor, label
la insertaremos en <dt>
y finalmente control
la envolveremos con
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 este código HTML:
<dl>
<dt><label class="required" for="frm-name">Nombre:</label></dt>
<dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd>
<dt><label class="required" for="frm-age">Edad:</label></dt>
<dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd>
<dt><label>Género:</label></dt>
...
</dl>
En el array wrappers se pueden influir en muchos otros atributos:
- agregar clases CSS a tipos individuales de elementos de formulario
- distinguir con clase CSS las filas pares e impares
- distinguir visualmente los elementos obligatorios y opcionales
- determinar si los mensajes de error se muestran directamente junto a los elementos o sobre el formulario
Opciones
El comportamiento del Renderer también se puede controlar estableciendo options en los elementos de formulario individuales. Así se puede establecer la descripción que se mostrará junto al campo de entrada:
$form->addText('phone', 'Número:')
->setOption('description', 'Este número permanecerá oculto');
Si queremos colocar contenido HTML en él, utilizaremos la clase Html:
use Nette\Utils\Html;
$form->addText('phone', 'Número:')
->setOption('description', Html::el('p')
->setHtml('<a href="...">Condiciones de almacenamiento de su número</a>')
);
El elemento Html también se puede usar en lugar de la etiqueta:
$form->addCheckbox('conditions', $label)
.
Agrupación de elementos
El Renderer permite agrupar elementos en grupos visuales (fieldsets):
$form->addGroup('Datos personales');
Después de crear un nuevo grupo, este se vuelve activo y cada elemento recién agregado también se agrega a él. Por lo tanto, el formulario se puede construir de esta manera:
$form = new Form;
$form->addGroup('Datos personales');
$form->addText('name', 'Su nombre:');
$form->addInteger('age', 'Su edad:');
$form->addEmail('email', 'Email:');
$form->addGroup('Dirección de envío');
$form->addCheckbox('send', 'Enviar a dirección');
$form->addText('street', 'Calle:');
$form->addText('city', 'Ciudad:');
$form->addSelect('country', 'País:', $countries);
El Renderer primero renderiza los grupos y solo después los elementos que no pertenecen a ningún grupo.
Soporte para Bootstrap
En los 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 de formulario, usamos el método
setHtmlAttribute(string $name, $value = true)
:
$form->addInteger('number', 'Número:')
->setHtmlAttribute('class', 'big-number');
$form->addSelect('rank', 'Ordenar por:', ['precio', 'nombre'])
->setHtmlAttribute('onchange', 'submit()'); // enviar al cambiar
// Para establecer atributos del propio <form>
$form->setHtmlAttribute('id', 'myForm');
Especificación del tipo de elemento:
$form->addText('tel', 'Su teléfono:')
->setHtmlType('tel')
->setHtmlAttribute('placeholder', 'escriba el teléfono');
Establecer el tipo y otros atributos sirve solo para fines visuales. La verificación de la corrección de las entradas debe realizarse en el servidor, lo que se asegura eligiendo el elemento de formulario adecuado e indicando las reglas de validación.
Podemos establecer atributos HTML con valores diferentes para cada uno de los elementos individuales en listas de radio
o checkbox. Observe los dos puntos después de style:
, que aseguran la elección del valor según la clave:
$colors = ['r' => 'rojo', 'g' => 'verde', 'b' => 'azul'];
$styles = ['r' => 'background:red', 'g' => 'background:green'];
$form->addCheckboxList('colors', 'Colores:', $colors)
->setHtmlAttribute('style:', $styles);
Imprime:
<label><input type="checkbox" name="colors[]" style="background:red" value="r">rojo</label>
<label><input type="checkbox" name="colors[]" style="background:green" value="g">verde</label>
<label><input type="checkbox" name="colors[]" value="b">azul</label>
Para establecer atributos booleanos, como readonly
, podemos usar la notación con un signo de interrogación:
$form->addCheckboxList('colors', 'Colores:', $colors)
->setHtmlAttribute('readonly?', 'r'); // para múltiples claves use un array, ej. ['r', 'g']
Imprime:
<label><input type="checkbox" name="colors[]" readonly value="r">rojo</label>
<label><input type="checkbox" name="colors[]" value="g">verde</label>
<label><input type="checkbox" name="colors[]" value="b">azul</label>
En el caso de los selectbox, el método setHtmlAttribute()
establece los atributos del elemento
<select>
. Si queremos establecer atributos para los <option>
individuales, usamos el método
setOptionAttribute()
. También funcionan las notaciones con dos puntos y signo de interrogación mencionadas
anteriormente:
$form->addSelect('colors', 'Colores:', $colors)
->setOptionAttribute('style:', $styles);
Imprime:
<select name="colors">
<option value="r" style="background:red">rojo</option>
<option value="g" style="background:green">verde</option>
<option value="b">azul</option>
</select>
Prototipos
Una forma alternativa de establecer atributos HTML consiste en 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', 'Número:');
$html = $input->getControlPrototype(); // <input>
$html->class('big-number'); // <input class="big-number">
De esta manera, también se puede modificar 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 elemento.
La devuelve getContainerPrototype()
. En el estado predeterminado, es un elemento “vacío”, por lo que no se
renderiza nada, pero al establecerle 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, también se puede influir en la plantilla del separador de elementos individuales, que
devuelve el método getSeparatorPrototype()
. En el estado predeterminado, es el elemento <br>
. Si
lo cambia a un elemento par, envolverá los elementos individuales en lugar de separarlos. Y además, se puede influir en la
plantilla del elemento HTML de la etiqueta en los elementos individuales, que devuelve getItemLabelPrototype()
.
Traducción
Si programa una aplicación multilingüe, probablemente necesitará renderizar el formulario en diferentes versiones lingüísticas. Nette Framework define para este propósito una interfaz para la traducción Nette\Localization\Translator. En Nette no hay una implementación predeterminada, puede elegir según sus necesidades entre varias soluciones listas que encontrará en Componette. En su documentación aprenderá cómo configurar el traductor.
Los formularios admiten la impresión de textos a través del traductor. Se lo pasamos usando el método
setTranslator()
:
$form->setTranslator($translator);
A partir de este momento, no solo todas las etiquetas, sino también todos los mensajes de error o elementos de select boxes se traducirán a otro idioma.
Para elementos de formulario individuales, es posible establecer un traductor diferente o desactivar completamente la
traducción con el valor null
:
$form->addSelect('carModel', 'Modelo:', $cars)
->setTranslator(null);
Para las reglas de validación, también se pasan parámetros específicos al traductor, por ejemplo, para la regla:
$form->addPassword('password', 'Contraseña:')
->addRule($form::MinLength, 'La contraseña debe tener al menos %d caracteres', 8);
se llama al traductor con estos parámetros:
$translator->translate('La contraseña debe tener al menos %d caracteres', 8);
y, por lo tanto, puede elegir la forma plural correcta de la palabra caracteres
según el número.
Evento onRender
Justo antes de que se renderice el formulario, podemos hacer que se llame nuestro código. Este puede, por ejemplo,
complementar los elementos del formulario con clases HTML para una correcta visualización. Agregamos el código al array
onRender
:
$form->onRender[] = function ($form) {
BootstrapCSS::initialize($form);
};