Renderizado de formularios
La apariencia de las formas puede ser muy diversa. En la práctica, podemos encontrarnos con dos extremos. Por un lado, existe
la necesidad de renderizar una serie de formularios en una aplicación que son visualmente similares entre sí, y apreciamos la
fácil renderización sin plantilla utilizando $form->render()
. Este suele ser el caso de las interfaces
administrativas.
Por otro lado, hay varios formularios en los que cada uno es único. Su aspecto se describe mejor utilizando el lenguaje HTML en la plantilla. Y, por supuesto, además de los dos extremos mencionados, nos encontraremos con muchos formularios que se sitúan en algún punto intermedio.
Renderizado con Latte
El sistema de plantillas Latte facilita fundamentalmente el renderizado de formularios y sus elementos. En primer lugar, mostraremos cómo renderizar formularios manualmente, elemento por elemento, para obtener un control total sobre el código. Más adelante mostraremos cómo automatizar dicho renderizado.
Usted puede tener la propuesta de una plantilla de Latte para el formulario generado utilizando el
método Nette\Forms\Blueprint::latte($form)
, que le dará salida a la página del navegador. A continuación, sólo
tiene que seleccionar el código con un clic y copiarlo en su proyecto.
{control}
La forma más sencilla de renderizar un formulario es escribirlo en una plantilla:
{control signInForm}
El aspecto del formulario renderizado puede cambiarse configurando el Renderer y los controles individuales.
n:name
Es extremadamente fácil enlazar la definición del formulario en código PHP con código HTML. Basta con añadir 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>Nombre de usuario: <input n:name=username size=20 autofocus></label>
</div>
<div>
<label n:name=password>Contraseña: <input n:name=password></label>
</div>
<div>
<input n:name=send class="btn btn-default">
</div>
</form>
El aspecto del código HTML resultante está totalmente en tus manos. Si utiliza el atributo n:name
con
<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
formulario dibujado y la etiqueta de cierre </form>
dibuja todos los elementos ocultos no dibujados (lo mismo
se aplica a {form} ... {/form}
).
Sin embargo, no debemos olvidar renderizar los posibles mensajes de error. Tanto los añadidos a elementos individuales por el
método addError()
(usando {inputError}
) como los añadidos 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>Nombre de usuario: <input n:name=username size=20 autofocus></label>
<span class=error n:ifcontent>{inputError username}</span>
</div>
<div>
<label n:name=password>Contraseña: <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, pueden renderizarse elemento a elemento:
{foreach $form[gender]->getItems() as $key => $label}
<label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label>
{/foreach}
{label}
{input}
¿No quieres pensar para cada elemento qué elemento HTML utilizar para él en la plantilla, ya sea <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}Nombre de usuario: {input username, size: 20, autofocus: true}{/label}
{inputError username}
</div>
<div>
{label password}Contraseña: {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.
De nuevo, los elementos de formulario más complejos, como RadioList o CheckboxList, se pueden renderizar elemento por elemento:
{foreach $form[gender]->items as $key => $label}
{label gender:$key}{input gender:$key} {$label}{/label}
{/foreach}
Para representar el <input>
en el elemento Checkbox, utilice {input myCheckbox:}
. Los atributos
HTML deben ir separados por una coma {input myCheckbox:, class: required}
.
{inputError}
Imprime un mensaje de error para el elemento del formulario, si tiene uno. El mensaje normalmente se envuelve en un elemento
HTML para estilizarlo. Evitar mostrar un elemento vacío si no hay mensaje puede hacerse elegantemente con
n:ifcontent
:
<span class=error n:ifcontent>{inputError $input}</span>
Podemos detectar la presencia de un error utilizando 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}
Etiquetas {form signInForm}...{/form}
son una alternativa a
<form n:name="signInForm">...</form>
.
Renderizado automático
Con las etiquetas {input}
y {label}
, podemos crear fácilmente una plantilla genérica para cualquier
formulario. Iterará y renderizará todos sus elementos secuencialmente, excepto los elementos ocultos, que se renderizan
automáticamente cuando el formulario termina con la etiqueta </form>
. Esperará el nombre del formulario
renderizado 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 de par de cierre automático utilizadas {label .../}
muestran las etiquetas procedentes de la
definición del formulario en el código PHP.
Puedes guardar esta plantilla genérica en el archivo basic-form.latte
y para renderizar el formulario, sólo
tienes que incluirla y pasar el nombre del formulario (o instancia) al parámetro $form
:
{include basic-form.latte, form: signInForm}
Si desea influir en la apariencia de un formulario en particular y dibujar un elemento de manera diferente, entonces la forma más fácil es preparar bloques en la plantilla que pueden ser sobrescritos más tarde. Los bloques también pueden tener nombres dinámicos, por lo que puede insertar en ellos el nombre del elemento a dibujar. Por ejemplo:
...
{label $input /}
{block "input-{$input->name}"}{input $input}{/block}
...
Para el elemento e.g. username
esto crea el bloque input-username
, que puede ser fácilmente anulado
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
puede definirse como un bloque, incluido el parámetro
$form
:
{define basic-form, $form}
<form n:name=$form class=form>
...
</form>
{/define}
Esto facilitará ligeramente su uso:
{embed basic-form, signInForm}
...
{/embed}
Sólo tendrá que importar el bloque en un lugar, al principio de la plantilla de diseño:
{import basic-form.latte}
Casos especiales
Si necesita mostrar sólo la parte interior del formulario sin etiquetas HTML <form>
por ejemplo, al enviar
fragmentos, ocúltelos con el atributo n:tag-if
:
<form n:name=signInForm n:tag-if=false>
<div>
<label n:name=username>Nombre de usuario: <input n:name=username></label>
{inputError username}
</div>
</form>
La etiqueta formContainer
ayuda con la representación de entradas dentro de un contenedor de formulario.
<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 sencilla de renderizar un formulario es llamar a:
$form->render();
El aspecto del formulario renderizado puede cambiarse configurando el Renderer y los controles individuales.
Renderizado manual
Cada elemento de formulario tiene métodos que generan el código HTML para el campo y la etiqueta del formulario. 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
Esto permite mostrar el formulario 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 entera de código HTML (CheckboxList,
RadioList). En este caso, puede utilizar métodos que generen entradas y etiquetas individuales, para cada elemento por
separado:
getControlPart($key = null): ?Html
devuelve el código HTML de un único elementogetLabelPart($key = null): ?Html
devuelve el código HTML de la etiqueta de un único elemento
Estos métodos llevan el prefijo get
por razones históricas, pero generate
sería
mejor, ya que crea y devuelve un nuevo elemento Html
en cada llamada.
Renderizador
Es un objeto que proporciona el renderizado del formulario. Puede ser establecido por el método
$form->setRenderer
. Se le pasa el control cuando se llama al método $form->render()
.
Si no establecemos un renderizador personalizado, se utilizará el renderizador por defecto Nette\Forms\Rendering\DefaultFormRenderer. Esto renderizará los elementos del formulario como una tabla HTML. El resultado es el siguiente:
<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>
...
Depende de usted, si desea utilizar una tabla o no, y muchos diseñadores web prefieren diferentes marcas, por ejemplo, una
lista. Podemos configurar DefaultFormRenderer
para que no se muestre en una tabla. Sólo tenemos que establecer $wrappers
adecuados. El primer índice siempre representa un área y el segundo un elemento. Todas las áreas respectivas se muestran en la
imagen:
Por defecto, un grupo de controls
se envuelve en <table>
y cada pair
es una fila de
tabla <tr>
que contiene un par de label
y control
(celdas <th>
y
<td>
). Vamos a cambiar todos esos elementos envolventes. Envolveremos controls
en
<dl>
dejaremos pair
solo, pondremos label
en <dt>
y envolveremos
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();
El resultado es el siguiente fragmento:
<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>
Las envolturas pueden afectar a muchos atributos. Por ejemplo:
- añadir clases CSS especiales a cada entrada del formulario
- distinguir entre líneas pares e impares
- hacer que lo obligatorio y lo opcional se dibujen de forma diferente
- establecer si los mensajes de error se muestran encima del formulario o cerca de cada elemento
Opciones
El comportamiento del Renderizador también puede controlarse estableciendo opciones en elementos individuales del formulario. De esta forma puede establecer el tooltip que se muestra junto al campo de entrada:
$form->addText('phone', 'Number:')
->setOption('description', 'This number will remain hidden');
Si queremos colocar contenido HTML en él, usamos la clase Html.
use Nette\Utils\Html;
$form->addText('phone', 'Phone:')
->setOption('description', Html::el('p')
->setHtml('<a href="...">Terms of service.</a>')
);
También se puede utilizar el elemento Html en lugar de la etiqueta:
$form->addCheckbox('conditions', $label)
.
Agrupación de entradas
El renderizador permite agrupar elementos en grupos visuales (fieldsets):
$form->addGroup('Personal data');
La creación de un nuevo grupo lo activa – todos los elementos añadidos posteriormente se añaden a este grupo. Usted puede construir un formulario como este:
$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 renderizador dibuja primero los grupos y después los elementos que no pertenecen a ningún grupo.
Soporte Bootstrap
Puede encontrar ejemplos de configuración de Renderer para Twitter Bootstrap 2, Bootstrap 3 y Bootstrap 4
Atributos HTML
Para establecer atributos HTML arbitrarios para elementos de formulario, utilice el método
setHtmlAttribute(string $name, $value = true)
:
$form->addInteger('number', 'Número:')
->setHtmlAttribute('class', 'número-grande');
$form->addSelect('range', 'Ordenar por:', ['precio', 'nombre'])
->setHtmlAttribute('onchange', 'submit()'); // llama a la función JS submit() 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', 'Por favor, introduzca su teléfono');
Establecer el tipo y otros atributos sólo sirve a efectos visuales. La verificación de la corrección de la entrada debe ocurrir en el servidor, lo que puede asegurar eligiendo un control de formulario apropiado y especificando reglas de validación.
Para elementos individuales en listas de radio o casillas de verificación, podemos establecer un atributo HTML con valores
diferentes para cada uno de ellos. Observe los dos puntos después de style:
, que garantiza que el valor se
selecciona en función de la clave:
$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>
Para establecer atributos booleanos, como readonly
, podemos utilizar la notación con un signo de
interrogación:
$form->addCheckboxList('colors', 'Colors:', $colors)
->setHtmlAttribute('readonly?', 'r'); // use array for multiple keys, e.g. ['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>
Para las cajas de selección, el método setHtmlAttribute()
establece los atributos del elemento
<select>
del elemento. Si queremos establecer los atributos para cada <option>
utilizaremos
el método setOptionAttribute()
. También funcionan los dos puntos y el signo de interrogación utilizados
anteriormente:
$form->addSelect('colors', 'Colores:', $colors)
->setOptionAttribute('style:', $styles);
Renders:
<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
Una forma alternativa 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 es devuelta por el método getControlPrototype()
:
$input = $form->addInteger('number');
$html = $input->getControlPrototype(); // <input>
$html->class('big-number'); // <input class="big-number">
La plantilla de etiqueta devuelta por getLabelPrototype()
también puede modificarse de esta forma:
$html = $input->getLabelPrototype(); // <label>
$html->class('distinctive'); // <label class="distinctive">
Para los elementos Checkbox, CheckboxList y RadioList se puede influir en la plantilla de elemento que envuelve al elemento. Es
devuelto por getContainerPrototype()
. Por defecto es un elemento “vacío”, por lo que no se renderiza nada, pero
dándole 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 es posible influir en el patrón separador de elementos devuelto por el método
getSeparatorPrototype()
. Por defecto, es un elemento <br>
. Si lo cambia a un elemento par,
envolverá los elementos individuales en lugar de separarlos. También es posible influir en el patrón del elemento HTML de las
etiquetas de los elementos, que devuelve getItemLabelPrototype()
.
Traducción
Si está programando una aplicación multilingüe, probablemente necesitará renderizar el formulario en diferentes idiomas. Nette Framework define una interfaz de traducción para este propósito Nette\Localization\Translator. No existe una implementación por defecto en Nette, puedes elegir según tus necesidades entre varias soluciones ya preparadas que puedes encontrar en Componette. Su documentación le explica cómo configurar el traductor.
El formulario soporta la salida de texto a través del traductor. Lo pasamos usando el método
setTranslator()
:
$form->setTranslator($translator);
A partir de ahora, no sólo todas las etiquetas, sino también todos los mensajes de error o las entradas de las casillas de selección se traducirán a otro idioma.
Es posible configurar un traductor diferente para elementos individuales del formulario o desactivar completamente la
traducción con null
:
$form->addSelect('carModel', 'Model:', $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', 'Password:')
->addRule($form::MinLength, 'Password has to be at least %d characters long', 8)
se llama al traductor con los siguientes parámetros:
$translator->translate('Password has to be at least %d characters long', 8);
y así puede elegir la forma plural correcta para la palabra characters
por recuento.
Evento onRender
Justo antes de que el formulario sea renderizado, podemos invocar nuestro código. Esto puede, por ejemplo, añadir clases HTML
a los elementos del formulario para su correcta visualización. Añadimos el código al array onRender
:
$form->onRender[] = function ($form) {
BootstrapCSS::initialize($form);
};