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.