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 elemento
  • getLabel($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 elemento
  • getLabelPart($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

Puede establecer cualquier atributo HTML a los controles de formulario utilizando 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


// aplicando en <form>
$form->setHtmlAttribute('id', 'myForm');

Configuración del tipo de entrada:

$form->addText('tel', 'Su teléfono:')
	->setHtmlType('tel')
	->setHtmlAttribute('placeholder', 'Por favor, introduzca su teléfono');

Podemos establecer el atributo HTML a elementos individuales en listas de radio o casillas de verificación con valores diferentes para cada uno de ellos. Tenga en cuenta los dos puntos después de style: para asegurarse de que el valor se selecciona por 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 un atributo HTML lógico (que no tiene valor, como readonly), puede utilizar un signo de interrogación:

$colors = ['r' => 'red', 'g' => 'green', 'b' => 'blue'];
$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');
echo $input->getControl();
// <label><input type="checkbox" name="send"></label>

$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);
};
versión: 4.0