Componentes interactivos

Los componentes son objetos reutilizables independientes que insertamos en las páginas. Pueden ser formularios, datagrids, encuestas, en realidad cualquier cosa que tenga sentido usar repetidamente. Mostraremos:

  • ¿cómo usar componentes?
  • ¿cómo escribirlos?
  • ¿qué son las señales?

Nette tiene incorporado un sistema de componentes. Algo similar pueden recordar los veteranos de Delphi o ASP.NET Web Forms, algo remotamente similar es la base de React o Vue.js. Sin embargo, en el mundo de los frameworks PHP, es una característica única.

Mientras tanto, los componentes influyen fundamentalmente en el enfoque para la creación de aplicaciones. Puede componer páginas a partir de unidades prefabricadas. ¿Necesita un datagrid en la administración? Lo encontrará en Componette, un repositorio de complementos de código abierto (es decir, no solo componentes) para Nette y simplemente insértelo en el presenter.

Puede incorporar cualquier número de componentes en el presenter. Y en algunos componentes puede insertar otros componentes. Esto crea un árbol de componentes, cuya raíz es el presenter.

Métodos de fábrica

¿Cómo se insertan los componentes en el presenter y se usan posteriormente? Generalmente mediante métodos de fábrica.

Una fábrica de componentes es una forma elegante de crear componentes solo cuando realmente se necesitan (lazy / on demand). Toda la magia reside en la implementación de un método llamado createComponent<Name>(), donde <Name> es el nombre del componente a crear, y que crea y devuelve el componente.

class DefaultPresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentPoll(): PollControl
	{
		$poll = new PollControl;
		$poll->items = $this->item;
		return $poll;
	}
}

Gracias a que todos los componentes se crean en métodos separados, el código gana en claridad.

Los nombres de los componentes siempre comienzan con una letra minúscula, aunque en el nombre del método se escriban con mayúscula.

Nunca llamamos a las fábricas directamente, se llaman solas en el momento en que usamos el componente por primera vez. Gracias a esto, el componente se crea en el momento adecuado y solo si realmente es necesario. Si no usamos el componente (por ejemplo, en una petición AJAX donde solo se transfiere una parte de la página, o al almacenar en caché la plantilla), no se crea en absoluto y ahorramos rendimiento del servidor.

// accedemos al componente y si fue la primera vez,
// se llama a createComponentPoll() que lo crea
$poll = $this->getComponent('poll');
// sintaxis alternativa: $poll = $this['poll'];

En la plantilla, es posible renderizar el componente usando la etiqueta {control}. Por lo tanto, no es necesario pasar manualmente los componentes a la plantilla.

<h2>Votar</h2>

{control poll}

Estilo Hollywood

Los componentes suelen utilizar una técnica fresca, que nos gusta llamar estilo Hollywood. Seguramente conoce la frase célebre que tan a menudo escuchan los participantes en las audiciones de cine: “No nos llame, nosotros le llamaremos”. Y de eso se trata precisamente.

En Nette, en lugar de tener que preguntar constantemente (“¿se envió el formulario?”, “¿fue válido?” o “¿presionó el usuario este botón?”), le dice al framework “cuando suceda, llama a este método” y deja el resto del trabajo en él. Si programa en JavaScript, conoce íntimamente este estilo de programación. Escribe funciones que se llaman cuando ocurre un evento determinado. Y el lenguaje les pasa los parámetros apropiados.

Esto cambia por completo la perspectiva sobre la escritura de aplicaciones. Cuantas más tareas pueda dejar en manos del framework, menos trabajo tendrá usted. Y menos cosas podrá olvidar.

Escribiendo un componente

Bajo el término componente, generalmente nos referimos a un descendiente de la clase Nette\Application\UI\Control. (Por lo tanto, sería más preciso usar el término “controls”, pero “controles” tiene un significado completamente diferente en español y más bien se ha impuesto “componentes”.) El propio presenter Nette\Application\UI\Presenter es, por cierto, también un descendiente de la clase Control.

use Nette\Application\UI\Control;

class PollControl extends Control
{
}

Renderizado

Ya sabemos que para renderizar un componente se usa la etiqueta {control componentName}. Esta en realidad llama al método render() del componente, en el que nos encargamos del renderizado. Tenemos disponible, al igual que en el presenter, una plantilla Latte en la variable $this->template, a la que pasamos parámetros. A diferencia del presenter, debemos indicar el archivo con la plantilla y hacer que se renderice:

public function render(): void
{
	// insertamos algunos parámetros en la plantilla
	$this->template->param = $value;
	// y la renderizamos
	$this->template->render(__DIR__ . '/poll.latte');
}

La etiqueta {control} permite pasar parámetros al método render():

{control poll $id, $message}
public function render(int $id, string $message): void
{
	// ...
}

A veces, un componente puede constar de varias partes que queremos renderizar por separado. Para cada una de ellas, creamos nuestro propio método de renderizado, aquí en el ejemplo, por ejemplo, renderPaginator():

public function renderPaginator(): void
{
	// ...
}

Y en la plantilla, luego la llamamos usando:

{control poll:paginator}

Para una mejor comprensión, es bueno saber cómo se traduce esta etiqueta a PHP.

{control poll}
{control poll:paginator 123, 'hello'}

se traduce como:

$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');

El método getComponent() devuelve el componente poll y sobre este componente llama al método render(), respectivamente renderPaginator() si se indica otro método de renderizado en la etiqueta después de los dos puntos.

Atención, si en cualquier lugar de los parámetros aparece =>, todos los parámetros se empaquetarán en un array y se pasarán como primer argumento:

{control poll, id: 123, message: 'hello'}

se traduce como:

$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);

Renderizado de un subcomponente:

{control cartControl-someForm}

se traduce como:

$control->getComponent("cartControl-someForm")->render();

Los componentes, al igual que los presenters, pasan automáticamente varias variables útiles a las plantillas:

  • $basePath es la ruta URL absoluta al directorio raíz (p. ej., /eshop)
  • $baseUrl es la URL absoluta al directorio raíz (p. ej., http://localhost/eshop)
  • $user es el objeto que representa al usuario
  • $presenter es el presenter actual
  • $control es el componente actual
  • $flashes array de mensajes enviados por la función flashMessage()

Señal

Ya sabemos que la navegación en una aplicación Nette consiste en enlazar o redirigir a pares Presenter:action. Pero, ¿qué pasa si solo queremos realizar una acción en la página actual? Por ejemplo, cambiar el orden de las columnas en una tabla; eliminar un elemento; cambiar el modo claro/oscuro; enviar un formulario; votar en una encuesta; etc.

Este tipo de peticiones se llaman señales. Y de manera similar a como las acciones invocan los métodos action<Action>() o render<Action>(), las señales llaman a los métodos handle<Signal>(). Mientras que el concepto de acción (o vista) está relacionado puramente con los presenters, las señales se aplican a todos los componentes. Y, por lo tanto, también a los presenters, porque UI\Presenter es un descendiente de UI\Control.

public function handleClick(int $x, int $y): void
{
	// ... procesamiento de la señal ...
}

El enlace que llama a la señal se crea de la manera habitual, es decir, en la plantilla con el atributo n:href o la etiqueta {link}, en el código con el método link(). Más en el capítulo Creación de enlaces URL.

<a n:href="click! $x, $y">haz clic aquí</a>

La señal siempre se llama en el presenter y la acción actuales, no es posible invocarla en otro presenter u otra acción.

Por lo tanto, la señal provoca la recarga de la página exactamente igual que en la petición original, solo que además llama al método de manejo de la señal con los parámetros correspondientes. Si el método no existe, se lanza una excepción Nette\Application\UI\BadSignalException, que se muestra al usuario como una página de error 403 Forbidden.

Fragmentos (Snippets) y AJAX

Las señales pueden recordarle un poco a AJAX: manejadores que se invocan en la página actual. Y tiene razón, las señales realmente se llaman a menudo usando AJAX y posteriormente transferimos al navegador solo las partes modificadas de la página. Es decir, los llamados fragmentos (snippets). Encontrará más información en la página dedicada a AJAX.

Mensajes flash

El componente tiene su propio almacenamiento de mensajes flash independiente del presenter. Son mensajes que, por ejemplo, informan sobre el resultado de una operación. Una característica importante de los mensajes flash es que están disponibles en la plantilla incluso después de una redirección. Incluso después de mostrarse, permanecen activos durante otros 30 segundos, por ejemplo, en caso de que el usuario actualice la página debido a una transmisión errónea, el mensaje no desaparecerá de inmediato.

El envío lo realiza el método flashMessage. El primer parámetro es el texto del mensaje o un objeto stdClass que representa el mensaje. El segundo parámetro opcional es su tipo (error, warning, info, etc.). El método flashMessage() devuelve una instancia del mensaje flash como un objeto stdClass, al que se le puede agregar información adicional.

$this->flashMessage('El elemento ha sido eliminado.');
$this->redirect(/* ... */); // y redirigimos

En la plantilla, estos mensajes están disponibles en la variable $flashes como objetos stdClass, que contienen las propiedades message (texto del mensaje), type (tipo de mensaje) y pueden contener la información de usuario ya mencionada. Los renderizamos, por ejemplo, así:

{foreach $flashes as $flash}
	<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}

Redirección después de una señal

Después de procesar una señal de componente, a menudo sigue una redirección. Es una situación similar a la de los formularios: después de enviarlos, también redirigimos para que al actualizar la página en el navegador no se vuelvan a enviar los datos.

$this->redirect('this') // redirige al presenter y acción actuales

Dado que el componente es un elemento reutilizable y generalmente no debería tener una vinculación directa con presenters específicos, los métodos redirect() y link() interpretan automáticamente el parámetro como una señal del componente:

$this->redirect('click') // redirige a la señal 'click' del mismo componente

Si necesita redirigir a otro presenter o acción, puede hacerlo a través del presenter:

$this->getPresenter()->redirect('Product:show'); // redirige a otro presenter/acción

Parámetros persistentes

Los parámetros persistentes sirven para mantener el estado en los componentes entre diferentes peticiones. Su valor permanece igual incluso después de hacer clic en un enlace. A diferencia de los datos en la sesión, se transfieren en la URL. Y esto de forma totalmente automática, incluidos los enlaces creados en otros componentes en la misma página.

Tiene, por ejemplo, un componente para paginar contenido. Puede haber varios de estos componentes en una página. Y deseamos que después de hacer clic en un enlace, todos los componentes permanezcan en su página actual. Por lo tanto, hacemos que el número de página (page) sea un parámetro persistente.

Crear un parámetro persistente es extremadamente simple en Nette. Basta con crear una propiedad pública y marcarla con un atributo: (anteriormente se usaba /** @persistent */)

use Nette\Application\Attributes\Persistent;  // esta línea es importante

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1; // debe ser public
}

Recomendamos indicar también el tipo de dato para la propiedad (p. ej., int) y puede indicar también un valor predeterminado. Los valores de los parámetros se pueden validar.

Al crear un enlace, se puede cambiar el valor del parámetro persistente:

<a n:href="this page: $page + 1">siguiente</a>

O se puede resetear, es decir, eliminar de la URL. Entonces tomará su valor predeterminado:

<a n:href="this page: null">resetear</a>

Componentes persistentes

No solo los parámetros, sino también los componentes pueden ser persistentes. En tal componente, sus parámetros persistentes se transfieren también entre diferentes acciones del presenter o entre varios presenters. Marcamos los componentes persistentes con una anotación en la clase del presenter. Por ejemplo, así marcamos los componentes calendar y poll:

/**
 * @persistent(calendar, poll)
 */
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}

Los subcomponentes dentro de estos componentes no necesitan ser marcados, también se volverán persistentes.

En PHP 8, también puede usar atributos para marcar componentes persistentes:

use Nette\Application\Attributes\Persistent;

#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}

Componentes con dependencias

¿Cómo crear componentes con dependencias sin “ensuciar” los presenters que los usarán? Gracias a las propiedades inteligentes del contenedor DI en Nette, al igual que al usar servicios clásicos, se puede dejar la mayor parte del trabajo al framework.

Tomemos como ejemplo un componente que tiene una dependencia del servicio PollFacade:

class PollControl extends Control
{
	public function __construct(
		private int $id, //  Id de la encuesta para la que creamos el componente
		private PollFacade $facade,
	) {
	}

	public function handleVote(int $voteId): void
	{
		$this->facade->vote($this->id, $voteId);
		// ...
	}
}

Si estuviéramos escribiendo un servicio clásico, no habría nada que resolver. El contenedor DI se encargaría invisiblemente de pasar todas las dependencias. Pero con los componentes, generalmente los tratamos de tal manera que creamos su nueva instancia directamente en el presenter en los métodos de fábrica createComponent…(). Pero pasar todas las dependencias de todos los componentes al presenter para luego pasarlas a los componentes es engorroso. Y la cantidad de código escrito…

La pregunta lógica es, ¿por qué simplemente no registramos el componente como un servicio clásico, lo pasamos al presenter y luego lo devolvemos en el método createComponent…()? Sin embargo, tal enfoque es inapropiado, porque queremos poder crear el componente incluso varias veces.

La solución correcta es escribir una fábrica para el componente, es decir, una clase que nos cree el componente:

class PollControlFactory
{
	public function __construct(
		private PollFacade $facade,
	) {
	}

	public function create(int $id): PollControl
	{
		return new PollControl($id, $this->facade);
	}
}

Así registramos la fábrica en nuestro contenedor en la configuración:

services:
	- PollControlFactory

y finalmente la usamos en nuestro presenter:

class PollPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private PollControlFactory $pollControlFactory,
	) {
	}

	protected function createComponentPollControl(): PollControl
	{
		$pollId = 1; // podemos pasar nuestro parámetro
		return $this->pollControlFactory->create($pollId);
	}
}

Lo genial es que Nette DI puede generar tales fábricas simples, por lo que en lugar de todo su código, basta con escribir solo su interfaz:

interface PollControlFactory
{
	public function create(int $id): PollControl;
}

Y eso es todo. Nette implementa internamente esta interfaz y la pasa al presenter, donde ya podemos usarla. Mágicamente, también agrega a nuestro componente el parámetro $id y la instancia de la clase PollFacade.

Componentes en profundidad

Los componentes en Nette Application representan partes reutilizables de una aplicación web que insertamos en las páginas y a las que, por cierto, se dedica todo este capítulo. ¿Qué capacidades exactas tiene tal componente?

  1. es renderizable en la plantilla
  2. sabe qué parte suya debe renderizar en una petición AJAX (fragmentos)
  3. tiene la capacidad de guardar su estado en la URL (parámetros persistentes)
  4. tiene la capacidad de reaccionar a las acciones del usuario (señales)
  5. crea una estructura jerárquica (donde la raíz es el presenter)

Cada una de estas funciones la realiza alguna de las clases de la línea de herencia. El renderizado (1 + 2) está a cargo de Nette\Application\UI\Control, la inclusión en el ciclo de vida (3, 4) de la clase Nette\Application\UI\Component y la creación de la estructura jerárquica (5) de las clases Container y Component.

Nette\ComponentModel\Component  { IComponent }
|
+- Nette\ComponentModel\Container  { IContainer }
	|
	+- Nette\Application\UI\Component  { SignalReceiver, StatePersistent }
		|
		+- Nette\Application\UI\Control  { Renderable }
			|
			+- Nette\Application\UI\Presenter  { IPresenter }

Ciclo de vida del componente

Ciclo de vida del componente

Validación de parámetros persistentes

Los valores de los parámetros persistentes recibidos de la URL se escriben en las propiedades mediante el método loadState(). Este también comprueba si el tipo de dato indicado en la propiedad coincide, de lo contrario responde con un error 404 y la página no se muestra.

Nunca confíe ciegamente en los parámetros persistentes, ya que pueden ser fácilmente sobrescritos por el usuario en la URL. Así, por ejemplo, verificamos si el número de página $this->page es mayor que 0. Una forma adecuada es sobrescribir el método mencionado loadState():

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1;

	public function loadState(array $params): void
	{
		parent::loadState($params); // aquí se establece $this->page
		// sigue la verificación propia del valor:
		if ($this->page < 1) {
			$this->error();
		}
	}
}

El proceso opuesto, es decir, la recopilación de valores de las propiedades persistentes, está a cargo del método saveState().

Señales en profundidad

Una señal provoca la recarga de la página exactamente igual que en la petición original (excepto en el caso de que se llame por AJAX) e invoca el método signalReceived($signal), cuya implementación predeterminada en la clase Nette\Application\UI\Component intenta llamar a un método compuesto por las palabras handle{signal}. El procesamiento posterior depende del objeto en cuestión. Los objetos que heredan de Component (es decir, Control y Presenter) reaccionan intentando llamar al método handle{signal} con los parámetros correspondientes.

En otras palabras: se toma la definición de la función handle{signal} y todos los parámetros que llegaron con la petición, y a los argumentos se les asignan los parámetros de la URL según el nombre e intenta llamar al método dado. Por ejemplo, como parámetro $id se pasa el valor del parámetro id en la URL, como $something se pasa something de la URL, etc. Y si el método no existe, el método signalReceived lanza una excepción.

La señal puede ser recibida por cualquier componente, presenter u objeto que implemente la interfaz SignalReceiver y esté conectado al árbol de componentes.

Los principales receptores de señales serán los Presenters y los componentes visuales que heredan de Control. La señal debe servir como una indicación para el objeto de que debe hacer algo: la encuesta debe contar el voto del usuario, el bloque de noticias debe expandirse y mostrar el doble de noticias, el formulario se envió y debe procesar los datos, y así sucesivamente.

La URL para la señal la creamos usando el método Component::link(). Como parámetro $destination pasamos la cadena {signal}! y como $args un array de argumentos que queremos pasar a la señal. La señal siempre se llama en el presenter y acción actuales con los parámetros actuales, los parámetros de la señal simplemente se agregan. Además, se agrega al principio el parámetro ?do, que determina la señal.

Su formato es {signal} o {signalReceiver}-{signal}. {signalReceiver} es el nombre del componente en el presenter. Por eso no puede haber un guion en el nombre del componente; se usa para separar el nombre del componente y la señal, sin embargo, es posible anidar varios componentes de esta manera.

El método isSignalReceiver() verifica si el componente (primer argumento) es el receptor de la señal (segundo argumento). Podemos omitir el segundo argumento; entonces verifica si el componente es receptor de cualquier señal. Como segundo parámetro se puede indicar true y así verificar si el receptor no es solo el componente indicado, sino también cualquiera de sus descendientes.

En cualquier fase anterior a handle{signal} podemos ejecutar la señal manualmente llamando al método processSignal(), que se encarga de gestionar la señal: toma el componente que se determinó como receptor de la señal (si no se especifica un receptor de señal, es el propio presenter) y le envía la señal.

Ejemplo:

if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
	$this->processSignal();
}

De esta manera, la señal se ejecuta prematuramente y ya no se volverá a llamar.

versión: 4.0