Componentes interactivos

Los componentes son objetos separados reutilizables que colocamos en las páginas. Pueden ser formularios, datagrids, encuestas, de hecho cualquier cosa que tenga sentido usar repetidamente. Lo mostraremos:

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

Nette tiene un sistema de componentes incorporado. Los más veteranos recordarán algo similar de Delphi o ASP.NET Web Forms. React o Vue.js están construidos sobre algo remotamente similar. Sin embargo, en el mundo de los frameworks PHP, esta es una característica completamente única.

Al mismo tiempo, los componentes cambian fundamentalmente el enfoque del desarrollo de aplicaciones. Puedes componer páginas a partir de unidades pre-preparadas. ¿Necesitas datagrid en la administración? Puedes encontrarlo en Componette, un repositorio de complementos de código abierto (no sólo componentes) para Nette, y simplemente pegarlo en el presentador.

Puede incorporar cualquier número de componentes en el presentador. Y puede insertar otros componentes en algunos componentes. Esto crea un árbol de componentes con un presentador como raíz.

Métodos de fábrica

¿Cómo se colocan y utilizan posteriormente los componentes en el presentador? Normalmente utilizando métodos de fábrica.

La fábrica de componentes es una forma elegante de crear componentes sólo cuando son realmente necesarios (lazy / on-demand). Toda la magia está en la implementación de un método llamado createComponent<Name>()donde <Name> es el nombre del componente, que creará y devolverá.

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

Como todos los componentes se crean en métodos separados, el código es más limpio y fácil de leer.

Los nombres de los componentes empiezan siempre con minúscula, aunque se escriben en mayúscula en el nombre del método.

Nunca llamamos a las fábricas directamente, se llaman automáticamente, cuando usamos componentes por primera vez. Gracias a ello, un componente se crea en el momento adecuado, y sólo si es realmente necesario. Si no usáramos el componente (por ejemplo en alguna petición AJAX, donde devolvemos sólo parte de la página, o cuando se almacenan partes en caché), ni siquiera se crearía y ahorramos rendimiento del servidor.

// accedemos al componente y si es la primera vez
// se llama a createComponentPoll() para crearlo
$poll = $this->getComponent('poll');
// sintaxis alternativa: $poll = $this['poll'];

En la plantilla, puedes renderizar un componente usando la etiqueta {control}. Así que no hay necesidad de pasar manualmente los componentes a la plantilla.

<h2>Please Vote</h2>

{control poll}

Estilo Hollywood

Los componentes suelen utilizar una técnica genial, que nos gusta llamar estilo Hollywood. Seguro que conoces el tópico que los actores oyen a menudo en los castings: “No nos llame a nosotros, nosotros le llamaremos a usted”. Y de eso se trata.

En Nette, en lugar de tener que hacer preguntas constantemente (“¿se ha enviado el formulario?”, “¿era válido?” o “¿alguien ha pulsado este botón?”), le dices al framework “cuando ocurra esto, llama a este método” y dejas que siga trabajando en ello. Si programas en JavaScript, estás familiarizado con este estilo de programación. Escribes funciones que se llaman cuando ocurre un determinado evento. Y el motor les pasa los parámetros apropiados.

Esto cambia por completo la forma de escribir aplicaciones. Cuantas más tareas puedas delegar en el framework, menos trabajo tendrás. Y menos puedes olvidar.

Cómo escribir un componente

Por componente solemos entender descendientes de la clase Nette\Application\UI\Control. El propio presentador Nette\Application\UI\Presenter también es descendiente de la clase Control.

use Nette\Application\UI\Control;

class PollControl extends Control
{
}

Presentación de

Ya sabemos que la etiqueta {control componentName} se utiliza para dibujar un componente. En realidad llama al método render() del componente, en el que nos encargamos de la renderización. Tenemos, al igual que en el presentador, una plantilla Latte en la variable $this->template, a la que pasamos los parámetros. A diferencia del uso en un presentador, debemos especificar un archivo de plantilla y dejar que se renderice:

public function render(): void
{
	// pondremos algunos parámetros en la plantilla
	$this->template->param = $value;
	// y la dibujaremos
	$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 crearemos nuestro propio método de renderizado, aquí está por ejemplo renderPaginator():

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

Y en la plantilla lo llamamos usando:

{control poll:paginator}

Para una mejor comprensión es bueno saber cómo se compila la etiqueta a código PHP.

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

Esto se compila a:

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

getComponent() el método devuelve el componente poll y luego se llama sobre él al método render() o renderPaginator(), respectivamente.

Si en algún lugar de la parte de parámetros se utiliza =>, todos los parámetros se envolverán con una matriz y se pasarán como primer argumento:

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

compila a:

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

Renderización del subcomponente:

{control cartControl-someForm}

compila a:

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

Los componentes, como los presentadores, pasan varias variables útiles a las plantillas automáticamente:

  • $basePath es una ruta URL absoluta al directorio raíz (por ejemplo /CD-collection)
  • $baseUrl es una URL absoluta al directorio raíz (por ejemplo http://localhost/CD-collection)
  • $user es un objeto que representa al usuario
  • $presenter es el presentador actual
  • $control es el componente actual
  • $flashes lista de mensajes enviados por el método flashMessage()

Señal

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

Este tipo de petición se llama señal. Y al igual que las acciones invocan métodos action<Action>() o render<Action>(), las señales llaman a métodos handle<Signal>(). Mientras que el concepto de acción (o vista) sólo se refiere a los presentadores, las señales se aplican a todos los componentes. Y, por tanto, también a los presentadores, porque UI\Presenter es descendiente de UI\Control.

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

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

<a n:href="click! $x, $y">click here</a>

La señal siempre se llama en el presentador y vista actuales, por lo que no es posible enlazar a la señal en un presentador / acción diferente.

Así, la señal hace que la página se recargue exactamente igual que en la petición original, sólo que además llama al método de gestión de la señal con los parámetros adecuados. Si el método no existe, se lanza la excepción Nette\Application\UI\BadSignalException, que se muestra al usuario como página de error 403 Forbidden.

Fragmentos y AJAX

Las señales pueden recordarte un poco a AJAX: manejadores que son llamados en la página actual. Y tienes razón, las señales se llaman muy a menudo usando AJAX, y entonces sólo transmitimos partes cambiadas de la página al navegador. Se llaman snippets. Puedes encontrar más información en la página sobre AJAX.

Mensajes Flash

Un componente dispone de su propio almacén de mensajes flash independiente del presentador. Se trata de mensajes que, por ejemplo, informan sobre el resultado de la operación. Una característica importante de los mensajes flash es que están disponibles en el modelo incluso después de la redirección. Incluso después de ser mostrados, permanecerán vivos durante otros 30 segundos – por ejemplo, en caso de que el usuario refrescara involuntariamente la página – el mensaje no se perderá.

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

$this->flashMessage('Item was deleted.');
$this->redirect(/* ... */); // y redirigir

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 dibujamos como sigue:

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

Parámetros persistentes

Los parámetros persistentes se utilizan para mantener el estado de los componentes entre diferentes peticiones. Su valor sigue siendo el mismo incluso después de hacer clic en un enlace. A diferencia de los datos de sesión, se transfieren en la URL. Y se transfieren automáticamente, incluidos los enlaces creados en otros componentes de la misma página.

Por ejemplo, tiene un componente de paginación de contenido. Puede haber varios componentes de este tipo en una página. Y quiere que todos los componentes permanezcan en su página actual cuando haga clic en el enlace. Por lo tanto, hacemos que el número de página (page) sea un parámetro persistente.

Crear un parámetro persistente es extremadamente fácil en Nette. Basta con crear una propiedad pública y etiquetarla con el atributo: (antes se utilizaba /** @persistent */ )

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

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

Te recomendamos que incluyas el tipo de dato (por ejemplo int) con la propiedad, y también puedes incluir un valor por defecto. Los valores de los parámetros se pueden validar.

Puede cambiar el valor de un parámetro persistente al crear un enlace:

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

O puede ser reset, es decir, eliminado de la URL. Entonces tomará su valor por defecto:

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

Componentes Persistentes

No sólo los parámetros, sino también los componentes pueden ser persistentes. Sus parámetros persistentes también se transfieren entre diferentes acciones o entre diferentes presentadores. Marcamos los componentes persistentes con estas anotaciones para la clase presentador. Por ejemplo aquí marcamos los componentes calendar y poll como sigue:

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

No es necesario marcar los subcomponentes como persistentes, son persistentes automáticamente.

En PHP 8, también puede utilizar 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 “fastidiar” a los presentadores que los utilizarán? Gracias a las inteligentes características del contenedor DI en Nette, al igual que con el uso de servicios tradicionales, podemos 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 un sondeo para el que se crea el componente
		private PollFacade $facade,
	) {
	}

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

Si estuviéramos escribiendo un servicio clásico, no habría nada de qué preocuparse. El contenedor DI se encargaría invisiblemente de pasar todas las dependencias. Pero normalmente manejamos los componentes creando una nueva instancia de ellos directamente en el presentador en los métodos de fábrica createComponent...(). Pero pasar todas las dependencias de todos los componentes al presentador para luego pasarlas a los componentes es engorroso. Y la cantidad de código escrito…

La pregunta lógica es, ¿por qué no registramos el componente como un servicio clásico, se lo pasamos al presentador y luego lo devolvemos en el método createComponent...()? Pero este enfoque es inadecuado porque queremos poder crear el componente varias veces.

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

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

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

Ahora registramos nuestro servicio al contenedor DI a la configuración:

services:
	- PollControlFactory

Por último, vamos a utilizar esta fábrica en nuestro presentador:

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

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

Lo bueno es que Nette DI puede generar fábricas tan simples, así que en lugar de escribir todo el código, sólo tienes que escribir su interfaz:

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

Eso es todo. Nette implementa internamente esta interfaz y la inyecta en nuestro presentador, donde podemos utilizarla. También pasa mágicamente nuestro parámetro $id y la instancia de la clase PollFacade a nuestro componente.

Componentes en profundidad

Los componentes en una aplicación Nette son las partes reutilizables de una aplicación web que incrustamos en las páginas, que es el tema de este capítulo. ¿Cuáles son exactamente las capacidades de un componente?

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

Cada una de estas funciones es gestionada por una de las clases del linaje de herencia. La renderización (1 + 2) es gestionada por Nette\Application\UI\Control, la incorporación al ciclo de vida (3, 4) por la clase Nette\Application\UI\Component, y la creación de la estructura jerárquica (5) por 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 las URLs son escritos en las propiedades por el método loadState(). También comprueba si el tipo de datos especificado para la propiedad coincide, de lo contrario responderá con un error 404 y la página no se mostrará.

Nunca confíes ciegamente en los parámetros persistentes porque pueden ser fácilmente sobrescritos por el usuario en la URL. Por ejemplo, así es como comprobamos si el número de página $this->page es mayor que 0. Una buena forma de hacerlo es sobrescribir el método loadState() mencionado anteriormente:

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

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

El proceso opuesto, es decir, recolectar valores de propiedades persistentes, es manejado por el método saveState().

Señales en profundidad

Una señal provoca una recarga de la página como la petición original (con la excepción de AJAX) e invoca el método signalReceived($signal) cuya implementación por defecto 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 dado. Los objetos descendientes de Component (es decir, Control y Presenter) intentan llamar a handle{Signal} con los parámetros pertinentes.

En otras palabras: se toma la definición del método handle{Signal} y se cotejan todos los parámetros que se recibieron en la solicitud con los parámetros del método. Esto significa que el parámetro id de la URL se empareja con el parámetro del método $id, something con $something y así sucesivamente. Y si el método no existe, el método signalReceived lanza una excepción.

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

Los principales receptores de señales son Presenters y los componentes visuales que amplían Control. Una señal es una señal para un objeto que tiene que hacer algo – la encuesta cuenta con un voto del usuario, la caja con noticias tiene que desplegarse, el formulario fue enviado y tiene que procesar datos y así sucesivamente.

La URL para la señal se crea usando el método Component::link(). Como parámetro $destination pasamos la cadena {signal}! y como $args un array de argumentos que queremos pasar al manejador de la señal. Los parámetros de la señal se adjuntan a la URL del presentador/vista actual. El parámetro ?do en la URL determina la señal llamada.

Su formato es {signal} o {signalReceiver}-{signal}. {signalReceiver} es el nombre del componente en el presentador. Esta es la razón por la que el guión (inexactamente dash) no puede estar presente en el nombre de los componentes – se utiliza para dividir el nombre del componente y la señal, pero es posible componer varios componentes.

El método isSignalReceiver() verifica si un componente (primer argumento) es receptor de una señal (segundo argumento). El segundo argumento puede omitirse – entonces averigua si el componente es receptor de alguna señal. Si el segundo parámetro es true verifica si el componente o sus descendientes son receptores de una señal.

En cualquier fase anterior a handle{Signal} se puede realizar la señal manualmente llamando al método processSignal() que se responsabiliza de la ejecución de la señal. Toma el componente receptor (si no está establecido es el propio presentador) y le envía la señal.

Ejemplo:

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

La señal se ejecuta prematuramente y no se volverá a llamar.

versión: 4.0