Dynamiczne snippety
Dość często podczas tworzenia aplikacji pojawia się potrzeba wykonywania operacji AJAX, na przykład na poszczególnych wierszach tabeli lub elementach listy. Jako przykład możemy wybrać listę artykułów, przy czym dla każdego z nich umożliwimy zalogowanemu użytkownikowi wybranie oceny “lubię/nie lubię”. Kod presentera i odpowiadającego mu szablonu bez AJAX będzie wyglądał mniej więcej tak (podaję najważniejsze fragmenty, kod zakłada istnienie usługi do oznaczania ocen i pobierania kolekcji artykułów – konkretna implementacja nie jest ważna dla celów tego poradnika):
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ż mi się to nie podoba</a>
{/if}
</article>
Ajaxizacja
Teraz wyposażmy tę prostą aplikację w AJAX. Zmiana oceny artykułu nie jest na tyle ważna, aby musiało dojść do
przekierowania, dlatego idealnie powinna odbywać się za pomocą AJAX w tle. Wykorzystamy skrypt obsługi z dodatków ze zwyczajową konwencją, że linki
AJAX mają klasę CSS ajax
.
Jednak jak to zrobić konkretnie? Nette oferuje 2 ścieżki: ścieżkę tzw. dynamicznych snippetów i ścieżkę komponentów. Obie mają swoje zalety i wady, dlatego pokażemy je po kolei.
Ścieżka dynamicznych snippetów
Dynamiczny snippet w terminologii Latte oznacza specyficzny przypadek użycia znacznika {snippet}
, gdzie w nazwie
snippetu używana jest zmienna. Taki snippet nie może znajdować się w szablonie byle gdzie – musi być opakowany statycznym
snippetem, tj. zwykłym, lub wewnątrz {snippetArea}
. Nasz szablon moglibyśmy zmodyfikować w następujący
sposób.
{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ż mi się to nie podoba</a>
{/if}
{/snippet}
</article>
{/snippet}
Każdy artykuł definiuje teraz jeden snippet, który ma w nazwie ID artykułu. Wszystkie te snippety są następnie razem
opakowane jednym snippetem o nazwie articlesContainer
. Gdybyśmy pominęli ten opakowujący snippet, Latte
poinformowałoby nas o tym wyjątkiem.
Pozostaje nam uzupełnić w prezenterze przerysowanie – wystarczy przerysować statyczną otoczkę.
public function handleLike(int $articleId): void
{
$this->ratingService->saveLike($articleId, $this->user->id);
if ($this->isAjax()) {
$this->redrawControl('articlesContainer');
// $this->redrawControl('article-' . $articleId); -- nie jest potrzebne
} else {
$this->redirect('this');
}
}
Podobnie zmodyfikujemy również siostrzaną metodę handleUnlike()
, i AJAX działa!
Rozwiązanie ma jednak jedną wadę. Gdybyśmy bardziej zbadali, jak przebiega żądanie AJAX, odkrylibyśmy, że chociaż na zewnątrz aplikacja wydaje się oszczędna (zwraca tylko jeden snippet dla danego artykułu), w rzeczywistości na serwerze wyrenderowała wszystkie snippety. Pożądany snippet umieściła w payloadzie, a pozostałe odrzuciła (całkowicie niepotrzebnie je również pobrała z bazy danych).
Aby zoptymalizować ten proces, będziemy musieli interweniować tam, gdzie przekazujemy do szablonu kolekcję
$articles
(powiedzmy w metodzie renderDefault()
). Wykorzystamy fakt, że przetwarzanie sygnałów odbywa
się przed 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');
}
}
Teraz podczas przetwarzania sygnału do szablonu przekazywana jest zamiast kolekcji ze wszystkimi artykułami tylko tablica
z jednym artykułem – tym, który chcemy wyrenderować i wysłać w payloadzie do przeglądarki. {foreach}
przebiegnie więc tylko raz i żadne dodatkowe snippety się nie wyrenderują.
Ścieżka komponentów
Zupełnie inny sposób rozwiązania unika dynamicznych snippetów. Sztuczka polega na przeniesieniu całej logiki do osobnego
komponentu – od teraz o wprowadzanie ocen nie będzie dbał presenter, ale dedykowany LikeControl
. Klasa będzie
wyglądać następująco (oprócz tego będzie zawierać również 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ż mi się to nie podoba</a>
{/if}
{/snippet}
Oczywiście zmieni nam się szablon widoku i do presentera będziemy musieli dodać fabrykę. Ponieważ komponent utworzymy tyle razy, ile artykułów pobierzemy z bazy danych, wykorzystamy do jego “rozmnożenia” klasę 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 zmniejszy się do niezbędnego minimum (i całkowicie pozbawiony snippetów!):
<article n:foreach="$articles as $article">
<h2>{$article->title}</h2>
<div class="content">{$article->content}</div>
{control "likeControl-$article->id"}
</article>
Mamy prawie gotowe: aplikacja teraz będzie działać AJAXowo. Również tutaj czeka nas optymalizacja aplikacji, ponieważ ze względu na użycie Nette Database podczas przetwarzania sygnału niepotrzebnie ładowane są wszystkie artykuły z bazy danych zamiast jednego. Zaletą jest jednak to, że nie dojdzie do ich renderowania, ponieważ wyrenderuje się rzeczywiście tylko nasz komponent.