Fragmente dinamice
Destul de des în dezvoltarea aplicațiilor există necesitatea de a efectua operații AJAX, de exemplu, în rândurile individuale ale unui tabel sau în elementele unei liste. Ca exemplu, putem alege să listăm articole, permițând utilizatorului logat să selecteze un rating “îmi place/nu-mi place” pentru fiecare dintre ele. Codul prezentatorului și șablonul corespunzător fără AJAX vor arăta cam așa (enumăr cele mai importante fragmente, codul presupune existența unui serviciu pentru marcarea ratingurilor și obținerea unei colecții de articole – implementarea specifică nu este importantă în scopul acestui 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');
}
Ș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>I like it</a>
{else}
<a n:href="unlike! $article->id" class=ajax>I don't like it anymore</a>
{/if}
</article>
Ajaxizare
Să aducem acum AJAX în această aplicație simplă. Modificarea ratingului unui articol nu este suficient de importantă
pentru a necesita o cerere HTTP cu redirecționare, așa că, în mod ideal, ar trebui să se facă cu AJAX în fundal. Vom
folosi scriptul handler din add-ons cu convenția obișnuită ca
legăturile AJAX să aibă clasa CSS ajax
.
Totuși, cum să o facem în mod specific? Nette oferă 2 modalități: modalitatea cu fragmente dinamice și modalitatea cu componente. Ambele au avantajele și dezavantajele lor, așa că le vom prezenta pe rând.
Metoda snippeturilor dinamice
În terminologia Latte, un fragment dinamic este un caz specific de utilizare a etichetei {snippet}
în care se
utilizează o variabilă în numele fragmentului. Un astfel de snippet nu poate fi găsit oriunde în șablon – trebuie să
fie inclus într-un snippet static, adică unul obișnuit, sau în interiorul unui {snippetArea}
. Am putea modifica
șablonul nostru după cum urmează.
{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>I like it</a>
{else}
<a n:href="unlike! $article->id" class=ajax>I don't like it anymore</a>
{/if}
{/snippet}
</article>
{/snippet}
Fiecare articol definește acum un singur snippet, care are un ID de articol în titlu. Toate aceste fragmente sunt apoi
grupate într-un singur fragment numit articlesContainer
. Dacă omitem acest snippet de împachetare, Latte ne va
alerta cu o excepție.
Tot ce mai rămâne de făcut este să adăugăm redesenarea în prezentator – doar 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');
}
}
Modificați metoda soră handleUnlike()
în același mod, iar AJAX este gata de funcționare!
Soluția are totuși un dezavantaj. Dacă cercetăm mai bine modul în care funcționează cererea AJAX, vom descoperi că, deși aplicația pare eficientă în aparență (returnează un singur fragment pentru un anumit articol), de fapt, redă toate fragmentele pe server. Acesta a plasat fragmentul dorit în sarcina noastră utilă și le-a eliminat pe celelalte (astfel, destul de inutil, le-a recuperat și din baza de date).
Pentru a optimiza acest proces, va trebui să luăm măsuri prin care să transmitem colecția $articles
șablonului (de exemplu, în metoda renderDefault()
). Vom profita de faptul că procesarea semnalului are loc
înainte de render<Something>
metodelor:
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, atunci când semnalul este procesat, în loc de o colecție cu toate articolele, doar o matrice cu un singur articol
este transmisă șablonului – cel pe care dorim să îl redăm și să îl trimitem în sarcină utilă către browser. Astfel,
{foreach}
se va face o singură dată și nu vor fi redate fragmente suplimentare.
Calea componentei
O soluție complet diferită utilizează o abordare diferită pentru a evita fragmentele dinamice. Trucul constă în mutarea
întregii logici într-o componentă separată – de acum încolo, nu mai avem un prezentator care să se ocupe de introducerea
ratingului, ci o componentă dedicată LikeControl
. Clasa va arăta ca mai jos (î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');
}
}
}
Șablon de componentă:
{snippet}
{if !$article->liked}
<a n:href="like!" class=ajax>I like it</a>
{else}
<a n:href="unlike!" class=ajax>I don't like it anymore</a>
{/if}
{/snippet}
Bineînțeles că vom schimba șablonul de vizualizare și va trebui să adăugăm o fabrică la prezentator. Deoarece vom crea componenta de câte ori vom primi articole 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 de vizualizare este redus la minimul necesar (și complet lipsit de fragmente!):
<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 în AJAX. Și aici trebuie să optimizăm aplicația, deoarece, datorită utilizării bazei de date Nette, procesarea semnalului va încărca inutil toate articolele din baza de date în loc de unul singur. Totuși, avantajul este că nu va exista nicio redare, deoarece doar componenta noastră este efectiv redată.