Snippets dynamiques
Comment utiliser AJAX pour ne rafraîchir que les parties d'une page qui changent réellement, par exemple les différents éléments d'une liste, à l'aide des snippets dynamiques de Latte.
Au cours du développement d'une application, le besoin se fait assez souvent sentir d'effectuer des opérations AJAX, par exemple sur les différentes lignes d'un tableau ou les éléments d'une liste. Prenons comme exemple la liste d'articles où les utilisateurs connectés peuvent noter chaque article par un “j'aime” ou un “je n'aime pas”. Le code du presenter et le template correspondant, sans AJAX, ressembleraient à ceci (nous montrons les parties les plus pertinentes ; le code suppose l'existence d'un service qui gère les notes et récupère les articles, son implémentation exacte n'ayant pas d'importance pour ce guide) :
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>J'aime</a>
{else}
<a n:href="unlike! $article->id" class=ajax>Je n'aime plus</a>
{/if}
</article>
Ajaxisation
Ajoutons maintenant l'AJAX à cette application toute simple. Changer la note d'un article n'est pas assez critique pour
justifier un rechargement complet de la page, cela devrait donc idéalement se faire en AJAX, en arrière-plan. Nous utiliserons
le script de gestion des add-ons avec la convention habituelle
selon laquelle les liens AJAX portent la classe CSS ajax.
Mais comment le mettre en œuvre concrètement ? Nette propose deux approches : les snippets dynamiques et les composants. Toutes deux ont leurs avantages et leurs inconvénients, nous allons donc montrer chacune d'elles.
La voie des snippets dynamiques
Dans la terminologie de Latte, un snippet dynamique désigne un usage particulier de la balise {snippet} où une
variable est utilisée dans le nom du snippet. Un tel snippet ne peut pas être placé n'importe où dans le template ; il doit
être enveloppé par un snippet statique (ordinaire) ou se trouver dans un {snippetArea}. Nous pourrions modifier
notre template ainsi :
{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>J'aime</a>
{else}
<a n:href="unlike! $article->id" class=ajax>Je n'aime plus</a>
{/if}
{/snippet}
</article>
{/snippet}
Chaque article définit désormais un snippet dont le nom contient l'ID de l'article. Tous ces snippets dynamiques sont ensuite
enveloppés ensemble par un snippet statique nommé articlesContainer. Si nous omettions ce snippet extérieur, Latte
lèverait une exception.
Il ne reste plus qu'à ajouter la logique de redessin au presenter : il suffit de redessiner l'enveloppe statique.
public function handleLike(int $articleId): void
{
$this->ratingService->saveLike($articleId, $this->user->id);
if ($this->isAjax()) {
$this->redrawControl('articlesContainer');
// $this->redrawControl('article-' . $articleId); -- inutile
} else {
$this->redirect('this');
}
}
Modifiez de la même façon la méthode handleUnlike() correspondante, et l'AJAX fonctionne !
Cette solution a cependant un inconvénient. Si nous examinons la requête AJAX de plus près, nous découvrirons que, même si l'application paraît efficace vue de l'extérieur (elle ne renvoie qu'un seul snippet pour l'article concerné), elle rend en réalité tous les snippets côté serveur. Elle place le snippet requis dans le payload et jette les autres (ce qui veut dire qu'elle les a aussi récupérés et rendus inutilement).
Pour optimiser cela, nous devons intervenir là où la collection $articles est passée au template (disons dans
la méthode renderDefault()). Nous tirerons parti du fait que le traitement du signal a lieu avant les méthodes
render<Quelquechose> :
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');
}
}
Désormais, pendant le traitement du signal, ce n'est plus toute la collection d'articles qui est passée au template, mais un
tableau contenant le seul article concerné, celui que nous voulons rendre et envoyer dans le payload au navigateur. Par
conséquent, la boucle {foreach} ne s'exécute qu'une fois et aucun snippet inutile n'est rendu.
La voie des composants
Une approche complètement différente évite totalement les snippets dynamiques. L'astuce consiste à encapsuler toute la
logique dans un composant distinct. Au lieu que le presenter gère la note, c'est un LikeControl dédié qui s'en
chargera. La classe ressemblera à ceci (elle contiendrait aussi les méthodes 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 du composant :
{snippet}
{if !$article->liked}
<a n:href="like!" class=ajax>J'aime</a>
{else}
<a n:href="unlike!" class=ajax>Je n'aime plus</a>
{/if}
{/snippet}
Naturellement, le template de la vue changera et nous devrons ajouter une factory au presenter. Comme nous créerons une instance de ce composant pour chaque article récupéré de la base de données, nous utiliserons la classe Multiplier pour gérer leur création.
protected function createComponentLikeControl()
{
$articles = $this->db->table('articles');
return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) {
return new LikeControl($articles[$articleId]);
});
}
Le template de la vue se réduit au strict minimum (et ne contient plus aucun snippet !) :
<article n:foreach="$articles as $article">
<h2>{$article->title}</h2>
<div class="content">{$article->content}</div>
{control "likeControl-$article->id"}
</article>
Nous avons presque terminé : l'application fonctionnera désormais en AJAX. Là aussi, une optimisation s'impose car, du fait de l'utilisation de Nette Database, le traitement du signal charge inutilement tous les articles depuis la base au lieu du seul article concerné. L'avantage, en revanche, est qu'aucun rendu inutile n'a lieu, puisque seule l'instance de composant concernée est rendue.