Snippet dinamici

Abbastanza spesso, durante lo sviluppo di applicazioni, sorge la necessità di eseguire operazioni AJAX, ad esempio, su singole righe di una tabella o elementi di un elenco. Come esempio, possiamo scegliere la visualizzazione di articoli, consentendo a ciascun utente loggato di scegliere una valutazione “mi piace/non mi piace”. Il codice del presenter e del template corrispondente senza AJAX apparirà approssimativamente come segue (riporto le parti più importanti, il codice presume l'esistenza di un servizio per contrassegnare le valutazioni e ottenere la collezione di articoli – l'implementazione specifica non è importante ai fini di questo 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>mi piace</a>
	{else}
		<a n:href="unlike! $article->id" class=ajax>non mi piace più</a>
	{/if}
</article>

Ajaxificazione

Ora dotiamo questa semplice applicazione di AJAX. La modifica della valutazione di un articolo non è così importante da richiedere un reindirizzamento, quindi idealmente dovrebbe avvenire tramite AJAX in background. Utilizzeremo lo script di gestione degli addon con la convenzione usuale che i link AJAX abbiano la classe CSS ajax.

Tuttavia, come farlo concretamente? Nette offre 2 percorsi: il percorso dei cosiddetti snippet dinamici e il percorso dei componenti. Entrambi hanno i loro pro e contro, quindi li mostreremo uno per uno.

Il percorso degli snippet dinamici

Uno snippet dinamico, nella terminologia Latte, significa un caso specifico di utilizzo del tag {snippet}, in cui viene utilizzata una variabile nel nome dello snippet. Tale snippet non può trovarsi ovunque nel template – deve essere racchiuso da uno snippet statico, cioè uno normale, o all'interno di {snippetArea}. Potremmo modificare il nostro template come segue.

{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>mi piace</a>
			{else}
				<a n:href="unlike! $article->id" class=ajax>non mi piace più</a>
			{/if}
		{/snippet}
	</article>
{/snippet}

Ogni articolo ora definisce uno snippet che ha l'ID dell'articolo nel nome. Tutti questi snippet sono poi racchiusi insieme da uno snippet chiamato articlesContainer. Se omettessimo questo snippet contenitore, Latte ci avviserebbe con un'eccezione.

Ci resta da aggiungere il ridisegno nel presenter – basta ridisegnare il contenitore statico.

public function handleLike(int $articleId): void
{
	$this->ratingService->saveLike($articleId, $this->user->id);
	if ($this->isAjax()) {
		$this->redrawControl('articlesContainer');
		// $this->redrawControl('article-' . $articleId); -- non è necessario
	} else {
		$this->redirect('this');
	}
}

Modifichiamo in modo simile anche il metodo gemello handleUnlike(), e AJAX è funzionante!

La soluzione ha però un lato oscuro. Se esaminassimo più da vicino come avviene la richiesta AJAX, scopriremmo che, sebbene esternamente l'applicazione sembri efficiente (restituisce solo un singolo snippet per l'articolo dato), in realtà sul server ha renderizzato tutti gli snippet. Ha inserito lo snippet desiderato nel payload e ha scartato gli altri (ottenendoli quindi inutilmente anche dal database).

Per ottimizzare questo processo, dovremo intervenire dove passiamo la collezione $articles al template (diciamo nel metodo renderDefault()). Sfrutteremo il fatto che l'elaborazione dei segnali avviene prima dei metodi 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');
	}
}

Ora, durante l'elaborazione del segnale, al template viene passato un array con un solo articolo – quello che vogliamo renderizzare e inviare nel payload al browser – invece della collezione con tutti gli articoli. {foreach} quindi verrà eseguito solo una volta e non verranno renderizzati snippet aggiuntivi.

Il percorso dei componenti

Un modo completamente diverso di risolvere il problema evita gli snippet dinamici. Il trucco sta nel trasferire l'intera logica in un componente separato – d'ora in poi non sarà il presenter a occuparsi dell'inserimento delle valutazioni, ma un LikeControl dedicato. La classe apparirà come segue (oltre a ciò, conterrà anche i metodi render, handleUnlike ecc.):

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 del componente:

{snippet}
	{if !$article->liked}
		<a n:href="like!" class=ajax>mi piace</a>
	{else}
		<a n:href="unlike!" class=ajax>non mi piace più</a>
	{/if}
{/snippet}

Naturalmente, il template della vista cambierà e dovremo aggiungere una factory al presenter. Poiché creeremo il componente tante volte quanti sono gli articoli ottenuti dal database, utilizzeremo la classe Multiplier per la sua “moltiplicazione”.

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

Il template della vista si riduce al minimo indispensabile (e completamente privo di snippet!):

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

Abbiamo quasi finito: l'applicazione ora funzionerà in modo AJAX. Anche qui dovremo ottimizzare l'applicazione, perché a causa dell'uso di Nette Database, durante l'elaborazione del segnale vengono caricati inutilmente tutti gli articoli dal database invece di uno solo. Il vantaggio, tuttavia, è che non verranno renderizzati, perché verrà renderizzato effettivamente solo il nostro componente.