Snippets dinámicos
Con bastante frecuencia, durante el desarrollo de aplicaciones, surge la necesidad de realizar operaciones AJAX, por ejemplo, sobre filas individuales de una tabla o elementos de una lista. Como ejemplo, podemos elegir mostrar artículos, permitiendo a cada usuario conectado elegir una calificación de “me gusta/no me gusta”. El código del Presenter y la plantilla correspondiente sin AJAX se verían aproximadamente así (presento los fragmentos más importantes, el código asume la existencia de un servicio para marcar calificaciones y obtener una colección de artículos; la implementación específica no es importante para los fines de este tutorial):
public function handleLike(int $articleId): void
{
$this->ratingService->saveLike($articleId, $this->user->id);
$this->redirect('this');
}
public function handleUnlike(int $articleId): void
{
$this->ratingService->removeLike($articleId, $this->user->id);
$this->redirect('this');
}
Plantilla:
<article n:foreach="$articles as $article">
<h2>{$article->title}</h2>
<div class="content">{$article->content}</div>
{if !$article->liked}
<a n:href="like! $article->id" class=ajax>me gusta</a>
{else}
<a n:href="unlike! $article->id" class=ajax>ya no me gusta</a>
{/if}
</article>
Ajaxificación
Ahora equipemos esta sencilla aplicación con AJAX. El cambio de calificación de un artículo no es tan importante como para
requerir una redirección, por lo que idealmente debería ocurrir mediante AJAX en segundo plano. Utilizaremos el script de manejo de complementos con la convención habitual de
que los enlaces AJAX tienen la clase CSS ajax
.
Pero, ¿cómo hacerlo específicamente? Nette ofrece 2 enfoques: el enfoque de los llamados snippets dinámicos y el enfoque de los componentes. Ambos tienen sus pros y sus contras, por lo que los mostraremos uno por uno.
El enfoque de los snippets dinámicos
Un snippet dinámico, en la terminología de Latte, significa un caso específico de uso de la etiqueta {snippet}
,
donde se utiliza una variable en el nombre del snippet. Dicho snippet no puede encontrarse en cualquier lugar de la plantilla;
debe estar envuelto por un snippet estático, es decir, uno normal, o dentro de {snippetArea}
. Podríamos modificar
nuestra plantilla de la siguiente manera.
{snippet articlesContainer}
<article n:foreach="$articles as $article">
<h2>{$article->title}</h2>
<div class="content">{$article->content}</div>
{snippet article-{$article->id}}
{if !$article->liked}
<a n:href="like! $article->id" class=ajax>me gusta</a>
{else}
<a n:href="unlike! $article->id" class=ajax>ya no me gusta</a>
{/if}
{/snippet}
</article>
{/snippet}
Cada artículo ahora define un snippet que tiene el ID del artículo en su nombre. Todos estos snippets se envuelven juntos en
un snippet llamado articlesContainer
. Si omitiéramos este snippet envolvente, Latte nos advertiría con una
excepción.
Nos queda añadir el redibujado al Presenter; basta con redibujar el envoltorio estático.
public function handleLike(int $articleId): void
{
$this->ratingService->saveLike($articleId, $this->user->id);
if ($this->isAjax()) {
$this->redrawControl('articlesContainer');
// $this->redrawControl('article-' . $articleId); -- no es necesario
} else {
$this->redirect('this');
}
}
Modificamos de manera similar el método hermano handleUnlike()
, ¡y AJAX funciona!
Sin embargo, la solución tiene un inconveniente. Si investigáramos más a fondo cómo se procesa la solicitud AJAX, descubriríamos que aunque la aplicación parece eficiente externamente (devuelve solo un snippet para el artículo dado), en realidad renderizó todos los snippets en el servidor. Colocó el snippet deseado en el payload y descartó los demás (obteniéndolos también innecesariamente de la base de datos).
Para optimizar este proceso, tendremos que intervenir donde pasamos la colección $articles
a la plantilla
(digamos, en el método renderDefault()
). Aprovecharemos el hecho de que el procesamiento de señales ocurre antes de
los métodos render<Something>
:
public function handleLike(int $articleId): void
{
// ...
if ($this->isAjax()) {
// ...
$this->template->articles = [
$this->db->table('articles')->get($articleId),
];
} else {
// ...
}
public function renderDefault(): void
{
if (!isset($this->template->articles)) {
$this->template->articles = $this->db->table('articles');
}
}
Ahora, al procesar la señal, en lugar de la colección con todos los artículos, se pasa a la plantilla solo un array con un
único artículo: el que queremos renderizar y enviar en el payload al navegador. Por lo tanto, {foreach}
se
ejecutará solo una vez y no se renderizarán snippets adicionales.
El enfoque de los componentes
Una forma completamente diferente de resolverlo evita los snippets dinámicos. El truco consiste en trasladar toda la lógica a
un componente separado: a partir de ahora, no será el Presenter el que se encargue de introducir las calificaciones, sino un
LikeControl
dedicado. La clase se verá así (además, también contendrá los métodos render
,
handleUnlike
, etc.):
class LikeControl extends Nette\Application\UI\Control
{
public function __construct(
private Article $article,
) {
}
public function handleLike(): void
{
$this->ratingService->saveLike($this->article->id, $this->presenter->user->id);
if ($this->presenter->isAjax()) {
$this->redrawControl();
} else {
$this->presenter->redirect('this');
}
}
}
Plantilla del componente:
{snippet}
{if !$article->liked}
<a n:href="like!" class=ajax>me gusta</a>
{else}
<a n:href="unlike!" class=ajax>ya no me gusta</a>
{/if}
{/snippet}
Por supuesto, la plantilla de la vista cambiará y tendremos que añadir una fábrica al Presenter. Dado que crearemos el componente tantas veces como artículos obtengamos de la base de datos, utilizaremos la clase Multiplier para su “multiplicación”.
protected function createComponentLikeControl()
{
$articles = $this->db->table('articles');
return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) {
return new LikeControl($articles[$articleId]);
});
}
La plantilla de la vista se reduce al mínimo indispensable (¡y completamente libre de snippets!):
<article n:foreach="$articles as $article">
<h2>{$article->title}</h2>
<div class="content">{$article->content}</div>
{control "likeControl-$article->id"}
</article>
Casi hemos terminado: la aplicación ahora funcionará con AJAX. Aquí también tendremos que optimizar la aplicación, porque debido al uso de Nette Database, al procesar la señal se cargan innecesariamente todos los artículos de la base de datos en lugar de uno solo. Sin embargo, la ventaja es que no se renderizarán, ya que realmente solo se renderizará nuestro componente.