Fragmentos dinámicos

Muy a menudo en el desarrollo de aplicaciones existe la necesidad de realizar operaciones AJAX, por ejemplo, en filas individuales de una tabla o en elementos de una lista. Como ejemplo, podemos elegir listar artículos, permitiendo al usuario logueado seleccionar una valoración “me gusta/no me gusta” para cada uno de ellos. El código del presentador y la plantilla correspondiente sin AJAX tendrán un aspecto similar al siguiente (enumero los fragmentos más importantes, el código asume la existencia de un servicio para marcar las valoraciones y obtener una colección de artículos – la implementación específica no es importante a efectos 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>I like it</a>
	{else}
		<a n:href="unlike! $article->id" class=ajax>I don't like it anymore</a>
	{/if}
</article>

Ajaxización

Llevemos ahora AJAX a esta sencilla aplicación. Cambiar la calificación de un artículo no es lo suficientemente importante como para requerir una petición HTTP con redirección, así que lo ideal sería hacerlo con AJAX en segundo plano. Usaremos el script handler de add-ons con la convención habitual de que los enlaces AJAX tienen la clase CSS ajax.

Sin embargo, ¿cómo hacerlo específicamente? Nette ofrece 2 maneras: la del fragmento dinámico y la del componente. Ambas tienen sus pros y sus contras, así que las mostraremos una a una.

La manera de los fragmentos dinámicos

En la terminología de Latte, un fragmento dinámico es un caso de uso específico de la etiqueta {snippet} en el que se utiliza una variable en el nombre del fragmento. Un fragmento de este tipo no puede encontrarse en cualquier lugar de la plantilla: debe estar envuelto por un fragmento estático, es decir, uno normal, o dentro de un {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>I like it</a>
			{else}
				<a n:href="unlike! $article->id" class=ajax>I don't like it anymore</a>
			{/if}
		{/snippet}
	</article>
{/snippet}

Cada artículo define ahora un único fragmento, que tiene un ID de artículo en el título. Todos estos fragmentos se agrupan en un único fragmento llamado articlesContainer. Si omitimos este fragmento, Latte nos avisará con una excepción.

Todo lo que queda por hacer es añadir el redibujado al presentador: basta con redibujar la envoltura estática.

public function handleLike(int $articleId): void
{
	$this->ratingService->saveLike($articleId, $this->user->id);
	if ($this->isAjax()) {
		$this->redrawControl('articlesContainer');
		// $this->redrawControl('article-' . $articleId); -- není potřeba
	} else {
		$this->redirect('this');
	}
}

Modifica el método hermano handleUnlike() de la misma manera, ¡y AJAX está listo y funcionando!

Sin embargo, la solución tiene un inconveniente. Si indagamos más en el funcionamiento de la petición AJAX, descubriremos que, aunque la aplicación parece eficiente en apariencia (sólo devuelve un único fragmento para un artículo determinado), en realidad renderiza todos los fragmentos en el servidor. Ha colocado el fragmento deseado en nuestra carga útil y ha descartado los demás (por lo que, de forma bastante innecesaria, también los ha recuperado de la base de datos).

Para optimizar este proceso, necesitaremos realizar una acción en la que pasemos la colección $articles a la plantilla (digamos en el método renderDefault() ). Aprovecharemos el hecho de que el procesamiento de la señal tiene lugar antes de que los render<Something> métodos:

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, cuando la señal es procesada, en lugar de una colección con todos los artículos, sólo un array con un único artículo es pasado a la plantilla – el que queremos renderizar y enviar en payload al navegador. Así, {foreach} se hará sólo una vez y no se renderizarán fragmentos extra.

Componente

Una solución completamente diferente utiliza un enfoque distinto para evitar los fragmentos dinámicos. El truco consiste en trasladar toda la lógica a un componente independiente: a partir de ahora, no tendremos un presentador que se encargue de introducir la calificación, sino una clase dedicada LikeControl. La clase tendrá el siguiente aspecto (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>I like it</a>
	{else}
		<a n:href="unlike!" class=ajax>I don't like it anymore</a>
	{/if}
{/snippet}

Por supuesto cambiaremos la plantilla de la vista y tendremos que añadir una fábrica al presentador. Como crearemos el componente tantas veces como artículos recibamos de la base de datos, utilizaremos la clase Multiplier para “multiplicarlo”.

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 vista de plantilla queda reducida al mínimo necesario (¡y completamente libre de fragmentos!):

<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 funcionará ahora en AJAX. Aquí también tenemos que optimizar la aplicación, porque debido al uso de Nette Database, el procesamiento de la señal cargará innecesariamente todos los artículos de la base de datos en lugar de uno. Sin embargo, la ventaja es que no habrá renderizado, porque sólo nuestro componente es realmente renderizado.