Componentes interactivos
Los componentes son objetos independientes y reutilizables que insertamos en las páginas. Pueden ser formularios, datagrids, encuestas, en definitiva cualquier cosa que tenga sentido usar repetidamente. Mostraremos:
- ¿cómo se usan los componentes?
- ¿cómo se escriben?
- ¿qué son las señales?
Nette lleva incorporado un sistema de componentes. Algo parecido puede resultar familiar a los veteranos de Delphi o ASP.NET Web Forms; React o Vue.js se apoyan en algo lejanamente similar. En el mundo de los frameworks de PHP, sin embargo, es una característica única.
Al mismo tiempo, los componentes influyen de manera esencial en la forma de desarrollar la aplicación. Puede componer las páginas a partir de unidades ya preparadas. ¿Necesita un datagrid en la administración? Búsquelo en Componette, un repositorio de complementos de código abierto (no solo componentes) para Nette, e insértelo sin más en el presenter.
Puede incorporar al presenter cualquier número de componentes. Y dentro de algunos componentes puede insertar otros componentes. Así surge un árbol de componentes cuya raíz es el presenter.
Métodos factory
¿Cómo se insertan los componentes en el presenter y cómo se usan después? Normalmente mediante métodos factory.
Una factory de componentes es una forma elegante de crear los componentes solo cuando realmente hacen falta (lazy / on demand).
Toda la magia consiste en implementar un método llamado createComponent<Name>(), donde
<Name> es el nombre del componente que se crea, y que crea y devuelve dicho componente.
class DefaultPresenter extends Nette\Application\UI\Presenter
{
protected function createComponentPoll(): PollControl
{
$poll = new PollControl;
$poll->items = $this->items;
return $poll;
}
}
Como todos los componentes se crean en métodos separados, el código gana en claridad.
Los nombres de los componentes empiezan siempre por minúscula, aunque en el nombre del método se escriban con mayúscula.
Nunca llamamos a las factories directamente; se invocan solas la primera vez que usamos el componente. Gracias a eso, el componente se crea en el momento adecuado y solo si realmente hace falta. Si no usamos el componente (por ejemplo, en una petición AJAX en la que solo se transfiere una parte de la página, o al guardar la plantilla en caché), no se creará en absoluto, lo que ahorra rendimiento del servidor.
// accedemos al componente y, si es la primera vez,
// se llama a createComponentPoll(), que lo crea
$poll = $this->getComponent('poll');
// sintaxis alternativa: $poll = $this['poll'];
En la plantilla se puede renderizar un componente con la etiqueta {control}. Por eso no hace falta pasar los componentes a la plantilla manualmente.
<h2>Please Vote</h2>
{control poll}
Para crear dinámicamente un número variable de componentes, use Multiplier.
Los métodos factory createComponent<Name>() no funcionan solo en los presenters. Del mismo modo puede
anidar un componente dentro de otro componente y componerlos en un árbol, lo que resulta práctico, por ejemplo, para un
formulario renderizado por separado dentro de un componente.
Estilo Hollywood
Los componentes usan habitualmente una técnica fresca que nos gusta llamar el estilo Hollywood. Seguro que conoce el tópico que oyen a menudo los participantes en los cástines de cine: “No nos llame, ya le llamaremos nosotros.” Y de eso se trata exactamente.
En Nette, en lugar de tener que preguntar constantemente (“¿se ha enviado el formulario?”, “¿era válido?” o “¿ha pulsado el usuario este botón?”), le dice al framework “cuando ocurra esto, llama a este método” y le deja a él el trabajo restante. Si programa en JavaScript, conoce a fondo este estilo de programación. Escribe funciones que se invocan cuando ocurre un determinado evento. Y el lenguaje les pasa los parámetros adecuados.
Esto cambia por completo la perspectiva desde la que se escriben las aplicaciones. Cuantas más tareas pueda dejar al framework, menos trabajo tendrá. Y menos cosas se le podrán pasar por alto.
Escribir un componente
Con el término componente nos referimos normalmente a un descendiente de la clase Nette\Application\UI\Control. (Sería más
exacto usar el término “controls”, pero en algunos idiomas tiene otro significado y “componentes” se ha impuesto más.)
El propio presenter Nette\Application\UI\Presenter es también
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 nombreComponente}. En realidad llama al
método render() del componente, en el que nos ocupamos del renderizado. Tenemos a disposición, igual que en el
presenter, una plantilla Latte en la variable
$this->template, a la que pasamos los parámetros. A diferencia del presenter, debemos indicar el archivo de
plantilla y mandar renderizarlo:
public function render(): void
{
// pasamos algunos parámetros a 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 su
propio método de renderizado, aquí en el ejemplo renderPaginator():
public function renderPaginator(): void
{
// ...
}
Y en la plantilla lo invocamos después con:
{control poll:paginator}
Para entenderlo mejor, conviene saber cómo se traduce esta etiqueta a código PHP.
{control poll}
{control poll:paginator 123, 'hello'}
se traduce a:
$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');
El método getComponent() devuelve el componente poll y sobre él se llama al método
render(), o bien renderPaginator() si en la etiqueta se indica tras los dos puntos otro método de
renderizado.
Atención: si en los parámetros aparece => fuera de corchetes, todos los parámetros se
envolverán en un array y se pasarán como primer argumento:
{control poll, id: 123, message: 'hello'}
se traduce a:
$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);
Renderizado de un subcomponente:
{control cartControl-someForm}
se traduce a:
$control->getComponent("cartControl-someForm")->render();
Los componentes, igual que los presenters, pasan automáticamente a las plantillas varias variables útiles:
$basePathes la ruta URL absoluta al directorio raíz (p. ej./eshop)$baseUrles la URL absoluta al directorio raíz (p. ej.http://localhost/eshop)$useres un objeto que representa al usuario$presenteres el presenter actual$controles el componente actual$flasheses un array de mensajes enviados con la funciónflashMessage()
Señal
Ya sabemos que la navegación en una aplicación Nette consiste en enlazar o redirigir a pares Presenter:action.
Pero ¿y si solo queremos realizar una acción en la página actual? Por ejemplo, cambiar la ordenación de las columnas de
una tabla; borrar un elemento; cambiar entre modo claro y oscuro; enviar un formulario; votar en una encuesta; etc.
Este tipo de petición se llama señal. Y del mismo modo que 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) se refiere puramente a los presenters, las
señales conciernen a todos los componentes. Y por tanto también a los presenters, porque UI\Presenter es
descendiente de UI\Control.
public function handleClick(int $x, int $y): void
{
// ... procesamiento de la señal ...
}
Un enlace que llama a una señal se crea de la forma habitual, es decir, en la plantilla con el atributo n:href
o la etiqueta {link}, y 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">click here</a>
Una señal se llama siempre sobre el presenter y la acción actuales; no es posible invocarla sobre otro presenter u otra acción.
La señal provoca, por tanto, la recarga de la página exactamente igual que la petición original, pero 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 una página de error 403 Forbidden.
Snippets y AJAX
Las señales pueden recordarle un poco a AJAX: gestores que se invocan en la página actual. Y tiene razón, las señales se llaman de hecho a menudo mediante AJAX y a continuación solo se transfieren al navegador las partes modificadas de la página. Se llaman snippets. Encontrará más información en la página dedicada a AJAX.
Mensajes flash
Un componente tiene su propio almacén de mensajes flash, independiente del presenter. Son mensajes que informan, por ejemplo, del resultado de una operación. Una propiedad importante de los mensajes flash es que están disponibles en la plantilla incluso después de una redirección. Incluso tras mostrarse siguen activos otros 30 segundos: por ejemplo, por si el usuario recarga la página debido a un error de transmisión, el mensaje no desaparecerá de inmediato.
Del envío se ocupa el método flashMessage. El primer
parámetro es el texto del mensaje (string, Stringable) 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 objeto stdClass, al que se le puede añadir
más información.
$this->flashMessage('Item was deleted.');
$this->redirect(/* ... */); // y redirige
Estos mensajes están disponibles en la plantilla en la variable $flashes como objetos stdClass, que
contienen las propiedades message (texto del mensaje) y type (tipo del 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 tras procesar una señal
Tras procesar la señal de un componente suele venir una redirección. Es algo parecido a los formularios: después de enviarlos también redirigimos, para evitar que se vuelvan a enviar los datos al recargar la página en el navegador.
$this->redirect('this'); // redirige al presenter y la acción actuales
Como un componente es un elemento reutilizable y normalmente no debería tener un vínculo directo con presenters concretos,
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 de los componentes entre distintas peticiones. Su valor sigue siendo el mismo incluso después de pulsar un enlace. A diferencia de los datos de la sesión, se transfieren en la URL. Y esto ocurre de forma completamente automática, incluidos los enlaces creados en otros componentes de la misma página.
Por ejemplo, tiene un componente para paginar contenido. Puede haber varios componentes así en una página. Y queremos que
todos los componentes sigan en su página actual después de pulsar un enlace. Por eso convertimos el número de página
(page) en un parámetro persistente.
Crear un parámetro persistente en Nette es facilísimo. Basta con crear una propiedad pública y marcarla con el atributo:
(antes 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 el tipo de dato de la propiedad (p. ej. int), y también puede indicar un valor por defecto.
Los valores de los parámetros se pueden validar.
Al crear un enlace se puede cambiar el valor de un parámetro persistente:
<a n:href="this page: $page + 1">next</a>
O se puede resetear, es decir, eliminar de la URL. Entonces adoptará su valor por defecto:
<a n:href="this page: null">reset</a>
Componentes persistentes
No solo los parámetros, también los componentes pueden ser persistentes. Sus parámetros persistentes se transfieren entonces
incluso entre distintas acciones del presenter o entre varios presenters. Los componentes persistentes los marcamos con un
atributo en la clase del presenter. Por ejemplo, marcamos así los componentes calendar y poll:
use Nette\Application\Attributes\Persistent;
#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
No hace falta marcar los subcomponentes dentro de estos componentes; también pasan a ser persistentes.
La anotación antigua @persistent sigue funcionando, pero está obsoleta y provoca una advertencia:
/**
* @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 van a usar? Gracias a las funciones inteligentes del contenedor DI de Nette, igual que ocurre con los servicios clásicos, se puede dejar la mayor parte del trabajo al framework.
Tomemos como ejemplo un componente que depende 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 discutir. El contenedor DI se encargaría de forma
invisible de pasar todas las dependencias. Pero con los componentes solemos actuar creando una nueva instancia directamente en el
presenter, dentro de los métodos factory createComponent…(). Pero pasar al
presenter todas las dependencias de todos los componentes solo para pasárselas después a los componentes es engorroso. Y la
cantidad de código que hay que escribir…
La pregunta lógica es por qué no registramos simplemente el componente como un servicio clásico, se lo pasamos al presenter
y luego lo devolvemos en el método createComponent…(). Este enfoque, sin embargo, no es adecuado, porque queremos
poder crear el componente varias veces si hace falta.
La solución correcta es escribir una factory 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);
}
}
Registramos esta factory 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 sabe generar estas factories tan sencillas, de modo que en lugar de escribir todo su código basta con escribir su interfaz:
interface PollControlFactory
{
public function create(int $id): PollControl;
}
Y eso es todo. Nette implementa internamente esta interfaz y la inyecta en el presenter, donde podemos usarla. Añade
mágicamente a nuestro componente el parámetro $id y una instancia de la clase PollFacade.
Los 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 está dedicado todo este capítulo. ¿Qué es exactamente capaz de hacer un componente así?
- se puede renderizar en una plantilla
- sabe qué parte de sí mismo debe renderizar durante una petición AJAX (snippets)
- tiene la capacidad de guardar su estado en la URL (parámetros persistentes)
- tiene la capacidad de reaccionar a las acciones del usuario (señales)
- crea una estructura jerárquica (cuya raíz es el presenter)
De cada una de estas funciones se encarga una de las clases de la línea de herencia. Del renderizado (1 + 2) se ocupa Nette\Application\UI\Control, de la integración en el ciclo de vida (3, 4) la clase Nette\Application\UI\Component, y de la creación de la estructura jerárquica (5) 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
Validación de los parámetros persistentes
Los valores de los parámetros persistentes recibidos de las URL los escribe en las
propiedades el método loadState(). Este comprueba también si coinciden con el tipo de dato indicado en la
propiedad; en caso contrario responde con un error 404 y la página no se muestra.
Nunca confíe ciegamente en los parámetros persistentes, porque el usuario puede sobrescribirlos fácilmente en la URL. Así
comprobamos, por ejemplo, si el número de página $this->page es mayor que 0. Una forma adecuada es sobrescribir
el mencionado método 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 comprobación propia del valor:
if ($this->page < 1) {
$this->error();
}
}
}
El proceso inverso, es decir, la recogida de los valores de las propiedades persistentes, lo realiza el método
saveState().
Conexión con el presenter
En el momento en que un componente pasa a formar parte de la jerarquía del presenter, se invocan sus callbacks guardados en el
array $onAnchor. A partir de ese momento el componente tiene el presenter a su disposición, puede crear enlaces con
seguridad, leer parámetros persistentes, etc.
$control->onAnchor[] = function ($control): void {
// el componente ya tiene el presenter a su disposición
};
Las señales en profundidad
La señal provoca la recarga de la página exactamente igual que la petición original (salvo cuando se llama por 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 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 adecuados.
Dicho de otro modo: se toma la definición de la función handle<Signal> junto con todos los parámetros que
llegaron con la petición, a los argumentos se les asignan por nombre los parámetros de la URL y se intenta llamar al método.
Por ejemplo, el valor del parámetro id de la URL se pasa como argumento $id, something de
la URL se pasa como $something, etc. Y si el método no existe, el método signalReceived lanza una excepción.
Además de los parámetros de la URL, la señal lee también los parámetros enviados en el cuerpo POST de la petición. Esto viene bien porque las señales se invocan a menudo mediante JavaScript, donde es natural enviar los datos por el método POST. Sin embargo, si llega un parámetro con el mismo nombre tanto de la URL como del cuerpo POST, tiene preferencia el valor de la URL. Por eso, evite dar a un campo POST el mismo nombre que a un parámetro de la URL o de la ruta; de lo contrario, el valor de la URL lo sobrescribiría silenciosamente. Los parámetros de las señales comparten un espacio común con los parámetros de acción y los persistentes, véase Espacio común de parámetros.
Una señal puede recibirla cualquier componente, presenter u objeto que implemente la interfaz SignalReceiver y
esté conectado al árbol de componentes.
Los principales destinatarios de las señales serán los Presenters y los componentes visuales que heredan de
Control. La señal está pensada como una indicación al 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 ha enviado y debe
procesar los datos, etc.
La URL de una señal se crea con 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 se llama siempre sobre el presenter y la acción actuales con los parámetros actuales; los
parámetros de la señal solo se añaden. Además se añade el parámetro ?do, que indica la señal.
Su formato es {signal} o {signalReceiver}-{signal}. {signalReceiver} es el nombre del
componente en el presenter. Por eso no puede usarse un guion en el nombre del componente: sirve para separar el nombre del
componente y la señal, aunque de esta forma sea posible anidar varios componentes.
El método isSignalReceiver()
comprueba si el componente (primer argumento) es el destinatario de la señal (segundo argumento). El segundo argumento se puede
omitir; entonces comprueba si el componente es el destinatario de alguna señal. Si el segundo parámetro se pone a
true, verifica si el componente indicado o alguno de sus descendientes es el destinatario.
En cualquier fase previa a handle<Signal> podemos ejecutar la señal manualmente llamando al método processSignal(), que
se ocupa de gestionar la señal: toma el componente identificado como destinatario de la señal (si no se indica ningún
destinatario, es el propio presenter) y le envía la señal.
Ejemplo:
if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
$this->processSignal();
}
Así se ejecuta la señal prematuramente y ya no se volverá a llamar.