Snippet dinamici
Come usare AJAX per aggiornare solo le parti di pagina che cambiano davvero, per esempio i singoli elementi di un elenco, grazie agli snippet dinamici di Latte.
Durante lo sviluppo di un'applicazione capita spesso di dover eseguire operazioni AJAX, per esempio sulle singole righe di una tabella o sugli elementi di un elenco. Come esempio prendiamo un elenco di articoli in cui gli utenti connessi possono valutare ogni articolo con “mi piace” o “non mi piace”. Il codice del presenter e il template corrispondente senza AJAX assomiglierebbero a questo (mostriamo le parti più importanti; il codice presuppone l'esistenza di un servizio per gestire le valutazioni e ottenere gli articoli, la cui implementazione concreta non è essenziale per questa guida):
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>
Ajaxizzazione
Aggiungiamo ora il supporto AJAX a questa semplice applicazione. Cambiare la valutazione di un articolo non è così importante
da richiedere un redirect dell'intera pagina, quindi idealmente dovrebbe avvenire con AJAX in background. Useremo lo script gestore dei componenti aggiuntivi con la convenzione consueta
secondo cui i link AJAX hanno la classe CSS ajax.
Ma come realizzarlo concretamente? Nette offre due strade: gli snippet dinamici e i componenti. Entrambe hanno pregi e difetti, quindi le mostreremo una per una.
La strada degli snippet dinamici
Nella terminologia di Latte, snippet dinamico indica un uso particolare del tag {snippet} in cui nel nome dello
snippet compare una variabile. Uno snippet del genere non si può collocare in un punto qualsiasi del template: deve essere
avvolto da uno snippet statico (normale) oppure trovarsi dentro un {snippetArea}. Il nostro template potrebbe essere
modificato così:
{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 definisce ora uno snippet il cui nome contiene l'ID dell'articolo. Tutti questi snippet dinamici sono poi avvolti
insieme da uno snippet statico chiamato articlesContainer. Se omettessimo lo snippet esterno, Latte lancerebbe
un'eccezione.
Non resta che aggiungere al presenter la logica di ridisegno: basta ridisegnare l'involucro 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');
}
}
Modificate allo stesso modo il metodo handleUnlike() corrispondente e AJAX funziona!
Questa soluzione ha però un punto debole. Se esaminiamo più da vicino la richiesta AJAX, scopriamo che l'applicazione, pur apparendo efficiente dall'esterno (restituisce un solo snippet per l'articolo in questione), sul server esegue in realtà il rendering di tutti gli snippet. Mette nel payload quello richiesto e scarta gli altri (cioè li ha anche inutilmente caricati e renderizzati).
Per ottimizzare la cosa dobbiamo intervenire dove la collezione $articles viene passata al template (diciamo nel
metodo renderDefault()). Sfrutteremo il fatto che la gestione del segnale avviene prima dei metodi
render<Qualcosa>:
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 non viene passata l'intera collezione di articoli, ma solo un array con il
singolo articolo che ci interessa, quello che vogliamo renderizzare e inviare nel payload al browser. Il ciclo
{foreach} viene quindi eseguito una sola volta e non si renderizza nessuno snippet superfluo.
La strada dei componenti
Un approccio del tutto diverso rinuncia agli snippet dinamici. Il trucco sta nel racchiudere tutta la logica in un componente
separato. Invece che il presenter, a occuparsi della valutazione sarà un apposito LikeControl. La classe apparirà
così (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 cambierà il template della vista e dovremo aggiungere una factory al presenter. Poiché creeremo un'istanza di questo componente per ogni articolo caricato dal database, useremo la classe Multiplier per gestirne la creazione.
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 non contiene alcuno 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 funzionerà ora con AJAX. Anche qui serve un'ottimizzazione, perché con l'uso di Nette Database la gestione del segnale carica inutilmente dal database tutti gli articoli invece del solo articolo che interessa. Il vantaggio, però, è che non avviene alcun rendering superfluo: viene renderizzata solo la specifica istanza del componente.