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.