Dinamični snippeti
Precej pogosto se pri razvoju aplikacij pojavi potreba po izvajanju AJAX operacij, na primer nad posameznimi vrsticami tabele ali elementi seznama. Za primer lahko izberemo izpis člankov, pri čemer pri vsakem od njih prijavljenemu uporabniku omogočimo izbiro ocene “všeč mi je/ni mi všeč”. Koda presenterja in ustrezne predloge brez AJAX-a bo izgledala približno takole (navajam najpomembnejše odseke, koda predvideva obstoj storitve za označevanje ocen in pridobivanje zbirke člankov – konkretna implementacija za namene tega navodila ni pomembna):
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');
}
Predloga:
<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>všeč mi je</a>
{else}
<a n:href="unlike! $article->id" class=ajax>ni mi več všeč</a>
{/if}
</article>
Ajaxizacija
Zdaj pa opremimo to preprosto aplikacijo z AJAX-om. Sprememba ocene članka ni tako pomembna, da bi moralo priti do
preusmeritve, zato bi idealno morala potekati z AJAX-om v ozadju. Uporabili bomo pomožni skript iz dodatkov z običajno konvencijo, da imajo AJAX
povezave CSS razred ajax
.
Vendar kako to storiti konkretno? Nette ponuja 2 poti: pot t.i. dinamičnih snippetov in pot komponent. Obe imata svoje prednosti in slabosti, zato si ju bomo ogledali eno za drugo.
Pot dinamičnih snippetov
Dinamični snippet v terminologiji Latte pomeni specifičen primer uporabe značke {snippet}
, kjer je v imenu
snippeta uporabljena spremenljivka. Takšen snippet se v predlogi ne more nahajati kjerkoli – mora biti ovit s statičnim
snippetom, tj. običajnim, ali znotraj {snippetArea}
. Našo predlogo bi lahko prilagodili na naslednji način.
{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>všeč mi je</a>
{else}
<a n:href="unlike! $article->id" class=ajax>ni mi več všeč</a>
{/if}
{/snippet}
</article>
{/snippet}
Vsak članek zdaj definira en snippet, ki ima v imenu ID članka. Vsi ti snippeti so nato skupaj zaviti v en snippet
z imenom articlesContainer
. Če bi ta ovojni snippet izpustili, bi nas Latte na to opozoril z izjemo.
Ostane nam še, da v presenter dodamo ponovno izrisovanje – dovolj je ponovno izrisati statični ovoj.
public function handleLike(int $articleId): void
{
$this->ratingService->saveLike($articleId, $this->user->id);
if ($this->isAjax()) {
$this->redrawControl('articlesContainer');
// $this->redrawControl('article-' . $articleId); -- ni potrebno
} else {
$this->redirect('this');
}
}
Podobno prilagodimo tudi sestrsko metodo handleUnlike()
, in AJAX deluje!
Rešitev pa ima eno senčno stran. Če bi podrobneje preučili, kako poteka AJAX zahteva, bi ugotovili, da čeprav se aplikacija navzven zdi varčna (vrne samo en sam snippet za določen članek), je v resnici na strežniku izrisala vse snippete. Želeni snippet nam je postavila v payload, ostale pa zavrgla (popolnoma nepotrebno jih je torej tudi pridobila iz podatkovne baze).
Da bi ta proces optimizirali, bomo morali poseči tja, kjer v predlogo posredujemo zbirko $articles
(recimo
v metodi renderDefault()
). Izkoristili bomo dejstvo, da obdelava signalov poteka pred metodami
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');
}
}
Zdaj se pri obdelavi signala v predlogo namesto zbirke z vsemi članki posreduje le polje z enim samim člankom – tistim,
ki ga želimo izrisati in poslati v payloadu v brskalnik. {foreach}
se torej izvede samo enkrat in nobeni dodatni
snippeti se ne izrišejo.
Pot komponent
Popolnoma drugačen način reševanja se izogne dinamičnim snippetom. Trik je v prenosu celotne logike v posebno
komponento – za vnos ocen ne bo več skrbel presenter, temveč namenska LikeControl
. Razred bo izgledal takole
(poleg tega bo vseboval tudi metode render
, handleUnlike
itd.):
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');
}
}
}
Predloga komponente:
{snippet}
{if !$article->liked}
<a n:href="like!" class=ajax>všeč mi je</a>
{else}
<a n:href="unlike!" class=ajax>ni mi več všeč</a>
{/if}
{/snippet}
Seveda se nam bo spremenila predloga pogleda (view) in v presenter bomo morali dodati tovarno. Ker bomo komponento ustvarili tolikokrat, kolikor člankov pridobimo iz podatkovne baze, bomo za njeno “razmnoževanje” uporabili razred Multiplier.
protected function createComponentLikeControl()
{
$articles = $this->db->table('articles');
return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) {
return new LikeControl($articles[$articleId]);
});
}
Predloga pogleda (view) se zmanjša na nujni minimum (in je popolnoma brez snippetov!):
<article n:foreach="$articles as $article">
<h2>{$article->title}</h2>
<div class="content">{$article->content}</div>
{control "likeControl-$article->id"}
</article>
Skoraj smo končali: aplikacija bo zdaj delovala AJAX-ovsko. Tudi tukaj nas čaka optimizacija aplikacije, saj se zaradi uporabe Nette Database pri obdelavi signala nepotrebno naložijo vsi članki iz podatkovne baze namesto enega. Prednost pa je, da ne pride do njihovega izrisovanja, ker se dejansko izriše samo naša komponenta.