Snippets Dynamiques

Assez souvent, lors du développement d'applications, le besoin se fait sentir d'effectuer des opérations AJAX, par exemple sur des lignes individuelles d'une table ou des éléments d'une liste. À titre d'exemple, nous pouvons choisir l'affichage d'articles, où pour chacun d'eux, nous permettons à l'utilisateur connecté de choisir une évaluation “j'aime/je n'aime pas”. Le code du presenter et du template correspondant sans AJAX ressemblera approximativement à ceci (je présente les extraits les plus importants, le code suppose l'existence d'un service pour marquer les évaluations et obtenir la collection d'articles – l'implémentation spécifique n'est pas importante aux fins de ce tutoriel) :

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>

Ajaxification

Ajoutons maintenant AJAX à cette application simple. Le changement d'évaluation d'un article n'est pas assez important pour nécessiter une redirection, il devrait donc idéalement se faire en arrière-plan via AJAX. Nous utiliserons le script de gestion des extensions avec la convention habituelle selon laquelle les liens AJAX ont la classe CSS ajax.

Mais comment faire concrètement ? Nette propose 2 voies : la voie des snippets dynamiques et la voie des composants. Les deux ont leurs avantages et leurs inconvénients, nous allons donc les présenter l'une après l'autre.

La voie des snippets dynamiques

Un snippet dynamique, dans la terminologie Latte, signifie un cas d'utilisation spécifique de la balise {snippet}, où une variable est utilisée dans le nom du snippet. Un tel snippet ne peut pas se trouver n'importe où dans le template – il doit être enveloppé par un snippet statique, c'est-à-dire ordinaire, ou à l'intérieur de {snippetArea}. Nous pourrions modifier notre template comme suit.

{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 maintenant un snippet qui a l'ID de l'article dans son nom. Tous ces snippets sont ensuite regroupés dans un seul snippet nommé articlesContainer. Si nous omettions ce snippet enveloppant, Latte nous le signalerait par une exception.

Il nous reste à ajouter le redessin dans le 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); -- pas nécessaire
	} else {
		$this->redirect('this');
	}
}

Nous modifions de la même manière la méthode sœur handleUnlike(), et AJAX est fonctionnel !

Cependant, la solution a un inconvénient. Si nous examinions plus en détail le déroulement de la requête AJAX, nous constaterions que bien que l'application semble économe en apparence (elle ne renvoie qu'un seul snippet pour l'article donné), elle a en fait rendu tous les snippets sur le serveur. Elle a placé le snippet souhaité dans le payload et a jeté les autres (les récupérant donc également inutilement de la base de données).

Pour optimiser ce processus, nous devrons intervenir là où nous passons la collection $articles au template (disons dans la méthode renderDefault()). Nous utiliserons le fait que le traitement des signaux a lieu avant les méthodes 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');
	}
}

Maintenant, lors du traitement du signal, au lieu de la collection avec tous les articles, seul un tableau avec un seul article est passé au template – celui que nous voulons rendre et envoyer dans le payload au navigateur. {foreach} ne s'exécutera donc qu'une seule fois et aucun snippet supplémentaire ne sera rendu.

La voie des composants

Une approche complètement différente évite les snippets dynamiques. L'astuce consiste à transférer toute la logique dans un composant séparé – la saisie des évaluations ne sera plus gérée par le presenter, mais par un LikeControl dédié. La classe ressemblera à ceci (en plus, elle contiendra également 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}

Bien sûr, le template de la vue changera et nous devrons ajouter une factory au presenter. Comme nous créerons le composant autant de fois que nous obtiendrons d'articles de la base de données, nous utiliserons la classe application:Multiplier pour sa “multiplication”.

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 est réduit au minimum nécessaire (et totalement dépourvu de snippets !) :

<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. Ici aussi, nous devons optimiser l'application, car en raison de l'utilisation de Nette Database, lors du traitement du signal, tous les articles sont inutilement chargés depuis la base de données au lieu d'un seul. L'avantage, cependant, est qu'ils ne seront pas rendus, car seul notre composant sera réellement rendu.