Nette Documentation Preview

syntax
Dynamiczne snippety
*******************

.[perex]
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):

```php
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:

```latte
<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 |application:ajax#Naja] 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:


```latte
{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.

```php
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ś>`:

```php
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.):

```php
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:

```latte
{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 |application:Multiplier].

```php
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!):

```latte
<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.

{{priority: -1}}

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.