Snippets dinâmicos

Com bastante frequência, durante o desenvolvimento de aplicações, surge a necessidade de realizar operações AJAX, por exemplo, em linhas individuais de uma tabela ou itens de uma lista. Como exemplo, podemos escolher a exibição de artigos, onde permitimos que um usuário logado escolha a avaliação “gosto/não gosto” para cada um deles. O código do presenter e do template correspondente sem AJAX será aproximadamente o seguinte (apresento os trechos mais importantes, o código assume a existência de um serviço para marcar a avaliação e obter a coleção de artigos – a implementação específica não é importante para os fins deste 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');
}

Template:

<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>Gosto disto</a>
	{else}
		<a n:href="unlike! $article->id" class=ajax>Já não gosto disto</a>
	{/if}
</article>

Ajaxificação

Vamos agora equipar esta aplicação simples com AJAX. A alteração da avaliação de um artigo não é tão importante a ponto de exigir um redirecionamento, e, portanto, idealmente, deveria ocorrer via AJAX em segundo plano. Usaremos o script auxiliar dos add-ons com a convenção usual de que os links AJAX têm a classe CSS ajax.

Mas como fazer isso especificamente? O Nette oferece 2 caminhos: o caminho dos chamados snippets dinâmicos e o caminho dos componentes. Ambos têm seus prós e contras, e por isso vamos mostrá-los um por um.

Caminho dos snippets dinâmicos

Um snippet dinâmico, na terminologia Latte, significa um caso específico de uso da tag {snippet}, onde uma variável é usada no nome do snippet. Tal snippet não pode estar em qualquer lugar no template – deve ser envolvido por um snippet estático, ou seja, um comum, ou dentro de {snippetArea}. Poderíamos modificar nosso template da seguinte forma.

{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>Gosto disto</a>
			{else}
				<a n:href="unlike! $article->id" class=ajax>Já não gosto disto</a>
			{/if}
		{/snippet}
	</article>
{/snippet}

Cada artigo agora define um snippet que tem o ID do artigo em seu nome. Todos esses snippets são então agrupados em um único snippet chamado articlesContainer. Se omitíssemos este snippet envolvente, o Latte nos alertaria com uma exceção.

Resta-nos adicionar o redesenho ao presenter – basta redesenhar o invólucro 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); -- não é necessário
	} else {
		$this->redirect('this');
	}
}

Modificamos de forma semelhante o método irmão handleUnlike(), e o AJAX está funcional!

A solução, no entanto, tem um lado sombrio. Se investigássemos mais a fundo como a requisição AJAX ocorre, descobriríamos que, embora externamente a aplicação pareça econômica (retorna apenas um único snippet para o artigo em questão), na realidade, no servidor, ela renderizou todos os snippets. Ela colocou o snippet desejado no payload e descartou os outros (obtendo-os desnecessariamente do banco de dados também).

Para otimizar este processo, teremos que intervir onde passamos a coleção $articles para o template (digamos, no método renderDefault()). Aproveitaremos o fato de que o processamento de sinais ocorre antes dos 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');
	}
}

Agora, ao processar o sinal, em vez de uma coleção com todos os artigos, apenas um array com um único artigo é passado para o template – aquele que queremos renderizar e enviar no payload para o navegador. O {foreach} então ocorrerá apenas uma vez e nenhum snippet extra será renderizado.

Caminho dos componentes

Uma forma completamente diferente de solução evita os snippets dinâmicos. O truque consiste em transferir toda a lógica para um componente separado – a partir de agora, o presenter não será responsável pela inserção da avaliação, mas sim um LikeControl dedicado. A classe ficará assim (além disso, conterá também os 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');
		}
	}
}

Template do componente:

{snippet}
	{if !$article->liked}
		<a n:href="like!" class=ajax>Gosto disto</a>
	{else}
		<a n:href="unlike!" class=ajax>Já não gosto disto</a>
	{/if}
{/snippet}

Claro, o template da view mudará e teremos que adicionar uma fábrica ao presenter. Como criaremos o componente tantas vezes quantos artigos obtivermos do banco de dados, usaremos a classe Multiplier para sua “multiplicação”.

protected function createComponentLikeControl()
{
	$articles = $this->db->table('articles');
	return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) {
		return new LikeControl($articles[$articleId]);
	});
}

O template da view será reduzido ao mínimo necessário (e completamente livre de snippets!):

<article n:foreach="$articles as $article">
	<h2>{$article->title}</h2>
	<div class="content">{$article->content}</div>
	{control "likeControl-$article->id"}
</article>

Estamos quase lá: a aplicação agora funcionará com AJAX. Aqui também teremos que otimizar a aplicação, porque devido ao uso do Nette Database, ao processar o sinal, todos os artigos são carregados desnecessariamente do banco de dados em vez de apenas um. A vantagem, no entanto, é que eles não serão renderizados, pois apenas nosso componente será renderizado.