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 ejemplohttp://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étodoflashMessage()
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}
Redirección tras una señal
Después de procesar una señal de componente, a menudo se produce una redirección. Esta situación es similar a la de los formularios: después de enviar un formulario, también redirigimos para evitar que se vuelvan a enviar los datos cuando se actualiza la página en el navegador.
$this->redirect('this') // redirects to the current presenter and action
Dado que un componente es un elemento reutilizable y, por lo general, no debería tener una dependencia directa de
presentadores específicos, los métodos redirect()
y link()
interpretan automáticamente el parámetro
como una señal de componente:
$this->redirect('click') // redirects to the 'click' signal of the same component
Si necesita redirigir a un presentador o acción diferente, puede hacerlo a través del presentador:
$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action
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\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 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?
- es renderizable en una plantilla
- sabe qué parte de sí mismo debe representar durante una petición AJAX (fragmentos)
- tiene la capacidad de almacenar su estado en una URL (parámetros persistentes)
- tiene la capacidad de responder a las acciones del usuario (señales)
- 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.