Snippets dinamice
Destul de des, în timpul dezvoltării aplicațiilor, apare nevoia de a efectua operațiuni AJAX, de exemplu, pe rândurile individuale ale unui tabel sau pe elementele unei liste. Ca exemplu, putem alege afișarea articolelor, permițând fiecărui utilizator autentificat să aleagă evaluarea “îmi place/nu-mi place”. Codul presenterului și șablonul corespunzător fără AJAX vor arăta aproximativ astfel (prezint cele mai importante fragmente, codul presupune existența unui serviciu pentru marcarea evaluărilor și obținerea colecției de articole – implementarea specifică nu este importantă pentru scopul acestui ghid):
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');
}
Șablon:
<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 place</a>
{else}
<a n:href="unlike! $article->id" class=ajax>nu-mi mai place</a>
{/if}
</article>
Ajaxizare
Să echipăm acum această aplicație simplă cu AJAX. Schimbarea evaluării unui articol nu este atât de importantă încât
să necesite o redirecționare, așa că ideal ar fi să se desfășoare prin AJAX în fundal. Vom folosi scriptul de ajutor din add-on-uri cu convenția obișnuită că
linkurile AJAX au clasa CSS ajax
.
Dar cum facem asta concret? Nette oferă 2 căi: calea așa-numitelor snippets dinamice și calea componentelor. Ambele au avantaje și dezavantaje, așa că le vom prezenta pe rând.
Calea snippetelor dinamice
Un snippet dinamic înseamnă, în terminologia Latte, un caz specific de utilizare a tag-ului {snippet}
, unde în
numele snippetului este folosită o variabilă. Un astfel de snippet nu poate fi găsit oriunde în șablon – trebuie să fie
încapsulat într-un snippet static, adică unul obișnuit, sau în interiorul {snippetArea}
. Am putea modifica
șablonul nostru astfel:
{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 place</a>
{else}
<a n:href="unlike! $article->id" class=ajax>nu-mi mai place</a>
{/if}
{/snippet}
</article>
{/snippet}
Fiecare articol definește acum un snippet care are ID-ul articolului în nume. Toate aceste snippets sunt apoi împachetate
împreună într-un singur snippet cu numele articlesContainer
. Dacă am omite acest snippet încapsulator, Latte
ne-ar avertiza cu o excepție.
Ne rămâne să adăugăm redesenarea în presenter – este suficient să redesenăm învelișul static.
public function handleLike(int $articleId): void
{
$this->ratingService->saveLike($articleId, $this->user->id);
if ($this->isAjax()) {
$this->redrawControl('articlesContainer');
// $this->redrawControl('article-' . $articleId); -- nu este necesar
} else {
$this->redirect('this');
}
}
Modificăm în mod similar și metoda soră handleUnlike()
, iar AJAX-ul este funcțional!
Soluția are însă un dezavantaj. Dacă am examina mai atent cum decurge cererea AJAX, am descoperi că, deși aplicația pare economică la exterior (returnează doar un singur snippet pentru articolul respectiv), în realitate, pe server, a redat toate snippet-urile. Snippet-ul dorit a fost plasat în payload, iar celelalte au fost aruncate (deci au fost obținute inutil din baza de date).
Pentru a optimiza acest proces, va trebui să intervenim acolo unde transmitem colecția $articles
către șablon
(să zicem în metoda renderDefault()
). Vom profita de faptul că procesarea semnalelor are loc înainte de metodele
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');
}
}
Acum, la procesarea semnalului, în loc de colecția cu toate articolele, se va transmite către șablon doar un array cu un
singur articol – cel pe care dorim să-l redăm și să-l trimitem în payload către browser. {foreach}
va rula
deci o singură dată și nu se vor mai reda snippet-uri suplimentare.
Calea componentelor
O modalitate complet diferită de rezolvare evită snippet-urile dinamice. Trucul constă în transferarea întregii logici
într-o componentă separată – de acum înainte, introducerea evaluărilor nu va mai fi gestionată de presenter, ci de o
LikeControl
dedicată. Clasa va arăta astfel (în plus, va conține și metodele 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');
}
}
}
Șablonul componentei:
{snippet}
{if !$article->liked}
<a n:href="like!" class=ajax>îmi place</a>
{else}
<a n:href="unlike!" class=ajax>nu-mi mai place</a>
{/if}
{/snippet}
Desigur, șablonul view-ului se va schimba și va trebui să adăugăm o fabrică în presenter. Deoarece vom crea componenta de atâtea ori câte articole obținem din baza de date, vom folosi clasa Multiplier pentru a o “multiplica”.
protected function createComponentLikeControl()
{
$articles = $this->db->table('articles');
return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) {
return new LikeControl($articles[$articleId]);
});
}
Șablonul view-ului se reduce la minimul necesar (și complet lipsit de snippet-uri!):
<article n:foreach="$articles as $article">
<h2>{$article->title}</h2>
<div class="content">{$article->content}</div>
{control "likeControl-$article->id"}
</article>
Aproape am terminat: aplicația va funcționa acum cu AJAX. Și aici va trebui să optimizăm aplicația, deoarece, datorită utilizării Nette Database, la procesarea semnalului se încarcă inutil toate articolele din baza de date în loc de unul singur. Avantajul este însă că acestea nu vor fi redate, deoarece se va reda efectiv doar componenta noastră.