Dynamiczne snippety
Jak za pomocą AJAX-u odświeżać tylko te części strony, które faktycznie się zmieniają, na przykład poszczególne pozycje listy, przy użyciu dynamicznych snippetów Latte.
Przy tworzeniu aplikacji dość często pojawia się potrzeba wykonywania operacji AJAX-owych na przykład na poszczególnych wierszach tabeli albo pozycjach listy. Jako przykład weźmy listę artykułów, w której zalogowany użytkownik może każdy artykuł ocenić jako „lubię to“ albo „nie lubię“. Kod presentera i odpowiadający mu szablon bez AJAX-u wyglądałyby mniej więcej tak (podajemy najważniejsze fragmenty; kod zakłada istnienie usługi obsługującej oceny i pobierającej artykuły, konkretna implementacja nie jest dla tego poradnika istotna):
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');
}
Szablon:
<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>lubię to</a>
{else}
<a n:href="unlike! $article->id" class=ajax>już tego nie lubię</a>
{/if}
</article>
Ajaksowanie
Dodajmy teraz do tej prostej aplikacji obsługę AJAX-u. Zmiana oceny artykułu nie jest na tyle istotna, by wymagała
przekierowania całej strony, więc powinna idealnie odbywać się w tle przez AJAX. Użyjemy skryptu obsługującego z dodatków wraz z powszechną konwencją, że
odnośniki AJAX-owe mają klasę CSS ajax.
Ale jak konkretnie to zrealizować? Nette oferuje dwa podejścia: dynamiczne snippety i komponenty. Oba mają swoje zalety i wady, więc pokażemy sobie oba.
Sposób z dynamicznymi snippetami
W terminologii Latte dynamiczny snippet oznacza szczególne użycie tagu {snippet}, w którym w nazwie snippetu
użyta jest zmienna. Takiego snippetu nie można umieścić w szablonie gdziekolwiek: musi być otoczony statycznym (zwykłym)
snippetem albo znajdować się wewnątrz {snippetArea}. Nasz szablon moglibyśmy zmodyfikować następująco:
{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>lubię to</a>
{else}
<a n:href="unlike! $article->id" class=ajax>już tego nie lubię</a>
{/if}
{/snippet}
</article>
{/snippet}
Każdy artykuł definiuje teraz snippet, którego nazwa zawiera ID artykułu. Wszystkie te dynamiczne snippety są następnie
wspólnie otoczone statycznym snippetem o nazwie articlesContainer. Gdybyśmy ten zewnętrzny snippet pominęli,
Latte rzuciłoby wyjątek.
Pozostaje już tylko dodać do presentera logikę przerysowywania: wystarczy przerysować statyczny wrapper.
public function handleLike(int $articleId): void
{
$this->ratingService->saveLike($articleId, $this->user->id);
if ($this->isAjax()) {
$this->redrawControl('articlesContainer');
// $this->redrawControl('article-' . $articleId); -- niepotrzebne
} else {
$this->redirect('this');
}
}
Analogicznie zmodyfikuj odpowiadającą metodę handleUnlike() i AJAX działa!
To rozwiązanie ma jednak pewną wadę. Gdy przyjrzymy się żądaniu AJAX-owemu bliżej, odkryjemy, że choć na zewnątrz aplikacja sprawia wrażenie oszczędnej (zwraca tylko jeden snippet dla konkretnego artykułu), po stronie serwera renderuje w rzeczywistości wszystkie snippety. Potrzebny snippet umieszcza w payloadzie, a pozostałe odrzuca (czyli niepotrzebnie je też pobrała i wyrenderowała).
Żeby to zoptymalizować, musimy zainterweniować tam, gdzie kolekcja $articles jest przekazywana do szablonu
(powiedzmy w metodzie renderDefault()). Wykorzystamy fakt, że obsługa sygnału odbywa się przed metodami
render<Coś>:
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');
}
}
Teraz przy obsłudze sygnału zamiast całej kolekcji artykułów trafia do szablonu tylko tablica z jednym istotnym
artykułem: tym, który zamierzamy wyrenderować i wysłać w payloadzie do przeglądarki. Dzięki temu pętla
{foreach} wykona się tylko raz i nie wyrenderują się żadne zbędne snippety.
Sposób z komponentami
Zupełnie inne podejście omija dynamiczne snippety całkowicie. Trik polega na zamknięciu całej logiki w osobnym
komponencie. Zamiast presentera obsługą oceniania zajmie się dedykowany LikeControl. Klasa będzie wyglądać tak
(zawierałaby także metody 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');
}
}
}
Szablon komponentu:
{snippet}
{if !$article->liked}
<a n:href="like!" class=ajax>lubię to</a>
{else}
<a n:href="unlike!" class=ajax>już tego nie lubię</a>
{/if}
{/snippet}
Naturalnie zmieni się szablon widoku i będziemy musieli dodać do presentera fabrykę. Ponieważ instancję tego komponentu utworzymy dla każdego artykułu pobranego z bazy danych, do zarządzania ich tworzeniem użyjemy klasy 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]);
});
}
Szablon widoku kurczy się do absolutnego minimum (i jest całkowicie wolny od snippetów!):
<article n:foreach="$articles as $article">
<h2>{$article->title}</h2>
<div class="content">{$article->content}</div>
{control "likeControl-$article->id"}
</article>
Jesteśmy prawie u celu: aplikacja będzie teraz działać z AJAX-em. I tutaj przyda się optymalizacja, bo ze względu na użycie Nette Database obsługa sygnału niepotrzebnie wczytuje z bazy wszystkie artykuły zamiast tylko tego istotnego. Zaletą jest natomiast to, że nie dochodzi do zbędnego renderowania, bo renderowana jest wyłącznie konkretna instancja komponentu.