Nette Documentation Preview

syntax
AJAX i snippety
***************

<div class=perex>

W erze nowoczesnych aplikacji webowych, w których funkcjonalność często rozłożona jest między serwer a przeglądarkę, AJAX jest niezbędnym elementem łączącym. Jakie możliwości oferuje w tym obszarze Nette Framework?
- wysyłanie części szablonu, zwanych snippetami
- przekazywanie zmiennych między PHP a JavaScriptem
- narzędzia do debugowania żądań AJAX

</div>


Żądanie AJAX
============

Żądanie AJAX zasadniczo nie różni się od klasycznego żądania HTTP. Wywoływany jest presenter z określonymi parametrami. To do presentera należy decyzja, jak odpowiedzieć na żądanie - może zwrócić dane w formacie JSON, wysłać fragment kodu HTML, dokument XML itd.

Po stronie przeglądarki inicjujemy żądanie AJAX funkcją `fetch()`:

```js
fetch(url, {
	headers: {'X-Requested-With': 'XMLHttpRequest'},
})
.then(response => response.json())
.then(payload => {
	// przetwarzamy odpowiedź
});
```

Po stronie serwera żądanie AJAX rozpoznaje metoda `$httpRequest->isAjax()` usługi [enkapsulującej żądanie HTTP |http:request]. Do wykrycia używa nagłówka HTTP `X-Requested-With`, dlatego kluczowe jest jego wysłanie. Wewnątrz presentera możesz użyć metody `$this->isAjax()`.

Jeśli chcesz wysłać dane w formacie JSON, użyj metody [`sendJson()` |presenters#Wysyłanie odpowiedzi]. Metoda ta kończy również działanie presentera.

```php
public function actionExport(): void
{
	$this->sendJson($this->model->getData());
}
```

Jeśli planujesz odpowiedzieć specjalnym szablonem przeznaczonym dla AJAX-a, możesz zrobić to tak:

```php
public function handleClick($param): void
{
	if ($this->isAjax()) {
		$this->template->setFile('path/to/ajax.latte');
	}
	// ...
}
```


Snippety
========

Najpotężniejszym narzędziem, jakie Nette oferuje do łączenia serwera z klientem, są snippety. Dzięki nim zamienisz zwykłą aplikację w aplikację AJAX-ową minimalnym wysiłkiem i kilkoma wierszami kodu. Jak to wszystko działa, pokazuje przykład Fifteen, którego kod znajdziesz na [GitHubie |https://github.com/nette-examples/fifteen].

Snippety pozwalają aktualizować tylko części strony zamiast przeładowywać ją całą. Jest to nie tylko szybsze i wydajniejsze, ale daje też wygodniejsze doświadczenie użytkownika. Snippety mogą przypominać Ci Hotwire dla Ruby on Rails albo Symfony UX Turbo. Co ciekawe, Nette wprowadziło snippety 14 lat wcześniej.

Jak działają snippety? Przy pierwszym wczytaniu strony (żądanie nie-AJAX) wczytywana jest cała strona wraz ze wszystkimi snippetami. Gdy użytkownik wejdzie w interakcję ze stroną (np. kliknie przycisk, wyśle formularz itd.), zamiast przeładowania całej strony inicjowane jest żądanie AJAX. Kod w presenterze wykonuje akcję i decyduje, które snippety trzeba zaktualizować. Nette renderuje te snippety i wysyła je jako payload JSON zawierający tablicę ze snippetami. Kod obsługujący w przeglądarce wstawia następnie otrzymane snippety z powrotem na stronę. Przesyłany jest więc tylko kod zmienionych snippetów, co oszczędza pasmo i przyspiesza ładowanie w porównaniu z przesyłaniem treści całej strony. Jeśli żaden snippet nie zostanie unieważniony przez `redrawControl()`, Nette zwraca całą stronę nawet przy żądaniu AJAX - snippety wysyłane są dopiero wtedy, gdy coś zostanie unieważnione.


Naja
----

Do obsługi snippetów po stronie przeglądarki służy [biblioteka Naja |https://naja.js.org]. [Zainstaluj ją |https://naja.js.org/#/guide/01-install-setup-naja] jako pakiet Node.js (do użycia z bundlerami, takimi jak Webpack, Rollup, Vite, Parcel i inne):

```shell
npm install naja
```

…albo wstaw ją bezpośrednio do szablonu strony:

```latte
<script src="https://unpkg.com/naja@3/dist/Naja.min.js"></script>
```

Najpierw trzeba bibliotekę [zainicjować |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization]:

```js
naja.initialize();
```

Aby zamienić zwykły odnośnik (sygnał) albo wysyłanie formularza w żądanie AJAX, wystarczy oznaczyć odpowiedni odnośnik, formularz albo przycisk klasą `ajax`:

```latte
<a n:href="go!" class="ajax">Idź</a>

<form n:name="form" class="ajax">
    <input n:name="submit">
</form>

albo

<form n:name="form">
    <input n:name="submit" class="ajax">
</form>
```


Przerysowywanie snippetów
-------------------------

Każdy obiekt klasy [Control |components] (w tym sam Presenter) pilnuje, czy zaszły zmiany wymagające jego przerysowania. Służy do tego metoda `redrawControl()`:

```php
public function handleLogin(string $user): void
{
	// po zalogowaniu trzeba przerysować odpowiednią część
	$this->redrawControl();
	// ...
}
```

Nette pozwala na jeszcze precyzyjniejszą kontrolę tego, co trzeba przerysować. Metoda może przyjąć jako argument nazwę snippetu. Można więc unieważniać (czyli wymuszać przerysowanie) na poziomie części szablonu. Jeśli unieważniony zostanie cały komponent, przerysowany zostanie również każdy snippet w jego wnętrzu:

```php
// unieważnia snippet 'header'
$this->redrawControl('header');
```

Oczekujące unieważnienie możesz też anulować drugim parametrem `$redraw`: wywołanie `$this->redrawControl('header', redraw: false)` oznacza snippet jako niewymagający przerysowania. Pełna sygnatura to `redrawControl(?string $snippet = null, bool $redraw = true)`.


Snippety w Latte
----------------

Używanie snippetów w Latte jest wyjątkowo łatwe. Aby zdefiniować część szablonu jako snippet, wystarczy opakować ją tagami `{snippet}` i `{/snippet}`:

```latte
{snippet header}
	<h1>Cześć ... </h1>
{/snippet}
```

Snippet tworzy na stronie HTML element `<div>` ze specjalnym wygenerowanym `id`. Przy przerysowaniu snippetu aktualizowana jest zawartość tego elementu. Dlatego konieczne jest, aby przy początkowym renderowaniu strony wyrenderowane zostały również wszystkie snippety, nawet jeśli na początku bywają puste.

Snippet możesz też utworzyć na elemencie innym niż `<div>`, używając n:atrybutu:

```latte
<article n:snippet="header" class="foo bar">
	<h1>Cześć ... </h1>
</article>
```


Obszary snippetów
-----------------

Nazwy snippetów mogą być również wyrażeniami:

```latte
{foreach $items as $id => $item}
	<li n:snippet="item-{$id}">{$item}</li>
{/foreach}
```

Samo w sobie jest to niedziałający krok pośredni: wyrenderowany poza statycznym `{snippet}` albo `{snippetArea}` dynamiczny snippet wywołuje `E_USER_WARNING` z komunikatem *Dynamic snippets are allowed only inside static snippet/snippetArea.* Poprawimy to poniżej.

Powstaje w ten sposób kilka snippetów w rodzaju `item-0`, `item-1` itd. Gdybyśmy unieważnili bezpośrednio dynamiczny snippet (np. `item-1`), nic nie zostałoby przerysowane. Powodem jest to, że snippety naprawdę działają jak wycinki i renderowane są bezpośrednio tylko one same. W szablonie technicznie nie ma jednak snippetu o nazwie `item-1`. Powstaje on dopiero wtedy, gdy wykona się kod otaczający snippet, czyli pętla foreach. Dlatego część szablonu, która ma zostać wykonana, oznaczamy tagiem `{snippetArea}`:

```latte
<ul n:snippetArea="itemsContainer">
	{foreach $items as $id => $item}
		<li n:snippet="item-{$id}">{$item}</li>
	{/foreach}
</ul>
```

I żądamy przerysowania zarówno pojedynczego snippetu, jak i całego obszaru nadrzędnego:

```php
$this->redrawControl('itemsContainer');
$this->redrawControl('item-1');
```

Jednocześnie warto zadbać o to, aby tablica `$items` zawierała tylko te elementy, które mają zostać przerysowane.

Jeśli do głównego szablonu dołączymy tagiem `{include}` inny szablon zawierający snippety, trzeba dołączenie szablonu ponownie opakować w `snippetArea` i unieważnić je razem ze snippetem:

```latte
{snippetArea include}
	{include 'included.latte'}
{/snippetArea}
```

```latte
{* included.latte *}
{snippet item}
	...
{/snippet}
```

```php
$this->redrawControl('include');
$this->redrawControl('item');
```


Snippety w komponentach
-----------------------

Snippety możesz tworzyć wewnątrz [komponentów|components], a Nette automatycznie je przerysuje. Jest jednak pewne ograniczenie: aby przerysować snippety, Nette wywołuje metodę `render()` bez żadnych parametrów. Przekazywanie parametrów w szablonie nie zadziała więc:

```latte
OK
{control productGrid}

nie zadziała:
{control productGrid $arg, $arg}
{control productGrid:paginator}
```


Wysyłanie własnych danych
-------------------------

Razem ze snippetami możesz wysłać do klienta dowolne dodatkowe dane. Wystarczy zapisać je do obiektu `payload`:

```php
public function actionDelete(int $id): void
{
	// ...
	if ($this->isAjax()) {
		$this->payload->message = 'Sukces';
	}
}
```


Przekierowywanie
----------------

Podczas żądania AJAX metody `redirect()` i `redirectUrl()` nie wysyłają przekierowania HTTP. Zamiast tego zapisują docelowy URL do payloadu (obiektu danych wysyłanego w odpowiedzi AJAX), konkretnie do jego właściwości `payload.redirect`, i wysyłają go; samo przekierowanie wykonuje następnie biblioteka po stronie klienta (Naja).


Przekazywanie parametrów
========================

Wysyłając do komponentu parametry przez żądanie AJAX, niezależnie od tego, czy są to parametry sygnału, czy parametry trwałe, musimy podać w żądaniu ich globalną nazwę, która zawiera również nazwę komponentu. Pełną nazwę parametru zwraca metoda `getParameterId()`.

```js
let url = new URL({link //foo!});
url.searchParams.set({$control->getParameterId('bar')}, bar);

fetch(url, {
	headers: {'X-Requested-With': 'XMLHttpRequest'},
})
```

A metoda handle z odpowiadającymi parametrami w komponencie:

```php
public function handleFoo(int $bar): void
{
}
```


Dalsza lektura
==============

- [Dynamiczne snippety |best-practices:dynamic-snippets]

AJAX i snippety

W erze nowoczesnych aplikacji webowych, w których funkcjonalność często rozłożona jest między serwer a przeglądarkę, AJAX jest niezbędnym elementem łączącym. Jakie możliwości oferuje w tym obszarze Nette Framework?

  • wysyłanie części szablonu, zwanych snippetami
  • przekazywanie zmiennych między PHP a JavaScriptem
  • narzędzia do debugowania żądań AJAX

Żądanie AJAX

Żądanie AJAX zasadniczo nie różni się od klasycznego żądania HTTP. Wywoływany jest presenter z określonymi parametrami. To do presentera należy decyzja, jak odpowiedzieć na żądanie – może zwrócić dane w formacie JSON, wysłać fragment kodu HTML, dokument XML itd.

Po stronie przeglądarki inicjujemy żądanie AJAX funkcją fetch():

fetch(url, {
	headers: {'X-Requested-With': 'XMLHttpRequest'},
})
.then(response => response.json())
.then(payload => {
	// przetwarzamy odpowiedź
});

Po stronie serwera żądanie AJAX rozpoznaje metoda $httpRequest->isAjax() usługi enkapsulującej żądanie HTTP. Do wykrycia używa nagłówka HTTP X-Requested-With, dlatego kluczowe jest jego wysłanie. Wewnątrz presentera możesz użyć metody $this->isAjax().

Jeśli chcesz wysłać dane w formacie JSON, użyj metody sendJson(). Metoda ta kończy również działanie presentera.

public function actionExport(): void
{
	$this->sendJson($this->model->getData());
}

Jeśli planujesz odpowiedzieć specjalnym szablonem przeznaczonym dla AJAX-a, możesz zrobić to tak:

public function handleClick($param): void
{
	if ($this->isAjax()) {
		$this->template->setFile('path/to/ajax.latte');
	}
	// ...
}

Snippety

Najpotężniejszym narzędziem, jakie Nette oferuje do łączenia serwera z klientem, są snippety. Dzięki nim zamienisz zwykłą aplikację w aplikację AJAX-ową minimalnym wysiłkiem i kilkoma wierszami kodu. Jak to wszystko działa, pokazuje przykład Fifteen, którego kod znajdziesz na GitHubie.

Snippety pozwalają aktualizować tylko części strony zamiast przeładowywać ją całą. Jest to nie tylko szybsze i wydajniejsze, ale daje też wygodniejsze doświadczenie użytkownika. Snippety mogą przypominać Ci Hotwire dla Ruby on Rails albo Symfony UX Turbo. Co ciekawe, Nette wprowadziło snippety 14 lat wcześniej.

Jak działają snippety? Przy pierwszym wczytaniu strony (żądanie nie-AJAX) wczytywana jest cała strona wraz ze wszystkimi snippetami. Gdy użytkownik wejdzie w interakcję ze stroną (np. kliknie przycisk, wyśle formularz itd.), zamiast przeładowania całej strony inicjowane jest żądanie AJAX. Kod w presenterze wykonuje akcję i decyduje, które snippety trzeba zaktualizować. Nette renderuje te snippety i wysyła je jako payload JSON zawierający tablicę ze snippetami. Kod obsługujący w przeglądarce wstawia następnie otrzymane snippety z powrotem na stronę. Przesyłany jest więc tylko kod zmienionych snippetów, co oszczędza pasmo i przyspiesza ładowanie w porównaniu z przesyłaniem treści całej strony. Jeśli żaden snippet nie zostanie unieważniony przez redrawControl(), Nette zwraca całą stronę nawet przy żądaniu AJAX – snippety wysyłane są dopiero wtedy, gdy coś zostanie unieważnione.

Naja

Do obsługi snippetów po stronie przeglądarki służy biblioteka Naja. Zainstaluj ją jako pakiet Node.js (do użycia z bundlerami, takimi jak Webpack, Rollup, Vite, Parcel i inne):

npm install naja

…albo wstaw ją bezpośrednio do szablonu strony:

<script src="https://unpkg.com/naja@3/dist/Naja.min.js"></script>

Najpierw trzeba bibliotekę zainicjować:

naja.initialize();

Aby zamienić zwykły odnośnik (sygnał) albo wysyłanie formularza w żądanie AJAX, wystarczy oznaczyć odpowiedni odnośnik, formularz albo przycisk klasą ajax:

<a n:href="go!" class="ajax">Idź</a>

<form n:name="form" class="ajax">
    <input n:name="submit">
</form>

albo

<form n:name="form">
    <input n:name="submit" class="ajax">
</form>

Przerysowywanie snippetów

Każdy obiekt klasy Control (w tym sam Presenter) pilnuje, czy zaszły zmiany wymagające jego przerysowania. Służy do tego metoda redrawControl():

public function handleLogin(string $user): void
{
	// po zalogowaniu trzeba przerysować odpowiednią część
	$this->redrawControl();
	// ...
}

Nette pozwala na jeszcze precyzyjniejszą kontrolę tego, co trzeba przerysować. Metoda może przyjąć jako argument nazwę snippetu. Można więc unieważniać (czyli wymuszać przerysowanie) na poziomie części szablonu. Jeśli unieważniony zostanie cały komponent, przerysowany zostanie również każdy snippet w jego wnętrzu:

// unieważnia snippet 'header'
$this->redrawControl('header');

Oczekujące unieważnienie możesz też anulować drugim parametrem $redraw: wywołanie $this->redrawControl('header', redraw: false) oznacza snippet jako niewymagający przerysowania. Pełna sygnatura to redrawControl(?string $snippet = null, bool $redraw = true).

Snippety w Latte

Używanie snippetów w Latte jest wyjątkowo łatwe. Aby zdefiniować część szablonu jako snippet, wystarczy opakować ją tagami {snippet} i {/snippet}:

{snippet header}
	<h1>Cześć ... </h1>
{/snippet}

Snippet tworzy na stronie HTML element <div> ze specjalnym wygenerowanym id. Przy przerysowaniu snippetu aktualizowana jest zawartość tego elementu. Dlatego konieczne jest, aby przy początkowym renderowaniu strony wyrenderowane zostały również wszystkie snippety, nawet jeśli na początku bywają puste.

Snippet możesz też utworzyć na elemencie innym niż <div>, używając n:atrybutu:

<article n:snippet="header" class="foo bar">
	<h1>Cześć ... </h1>
</article>

Obszary snippetów

Nazwy snippetów mogą być również wyrażeniami:

{foreach $items as $id => $item}
	<li n:snippet="item-{$id}">{$item}</li>
{/foreach}

Samo w sobie jest to niedziałający krok pośredni: wyrenderowany poza statycznym {snippet} albo {snippetArea} dynamiczny snippet wywołuje E_USER_WARNING z komunikatem Dynamic snippets are allowed only inside static snippet/snippetArea. Poprawimy to poniżej.

Powstaje w ten sposób kilka snippetów w rodzaju item-0, item-1 itd. Gdybyśmy unieważnili bezpośrednio dynamiczny snippet (np. item-1), nic nie zostałoby przerysowane. Powodem jest to, że snippety naprawdę działają jak wycinki i renderowane są bezpośrednio tylko one same. W szablonie technicznie nie ma jednak snippetu o nazwie item-1. Powstaje on dopiero wtedy, gdy wykona się kod otaczający snippet, czyli pętla foreach. Dlatego część szablonu, która ma zostać wykonana, oznaczamy tagiem {snippetArea}:

<ul n:snippetArea="itemsContainer">
	{foreach $items as $id => $item}
		<li n:snippet="item-{$id}">{$item}</li>
	{/foreach}
</ul>

I żądamy przerysowania zarówno pojedynczego snippetu, jak i całego obszaru nadrzędnego:

$this->redrawControl('itemsContainer');
$this->redrawControl('item-1');

Jednocześnie warto zadbać o to, aby tablica $items zawierała tylko te elementy, które mają zostać przerysowane.

Jeśli do głównego szablonu dołączymy tagiem {include} inny szablon zawierający snippety, trzeba dołączenie szablonu ponownie opakować w snippetArea i unieważnić je razem ze snippetem:

{snippetArea include}
	{include 'included.latte'}
{/snippetArea}
{* included.latte *}
{snippet item}
	...
{/snippet}
$this->redrawControl('include');
$this->redrawControl('item');

Snippety w komponentach

Snippety możesz tworzyć wewnątrz komponentów, a Nette automatycznie je przerysuje. Jest jednak pewne ograniczenie: aby przerysować snippety, Nette wywołuje metodę render() bez żadnych parametrów. Przekazywanie parametrów w szablonie nie zadziała więc:

OK
{control productGrid}

nie zadziała:
{control productGrid $arg, $arg}
{control productGrid:paginator}

Wysyłanie własnych danych

Razem ze snippetami możesz wysłać do klienta dowolne dodatkowe dane. Wystarczy zapisać je do obiektu payload:

public function actionDelete(int $id): void
{
	// ...
	if ($this->isAjax()) {
		$this->payload->message = 'Sukces';
	}
}

Przekierowywanie

Podczas żądania AJAX metody redirect() i redirectUrl() nie wysyłają przekierowania HTTP. Zamiast tego zapisują docelowy URL do payloadu (obiektu danych wysyłanego w odpowiedzi AJAX), konkretnie do jego właściwości payload.redirect, i wysyłają go; samo przekierowanie wykonuje następnie biblioteka po stronie klienta (Naja).

Przekazywanie parametrów

Wysyłając do komponentu parametry przez żądanie AJAX, niezależnie od tego, czy są to parametry sygnału, czy parametry trwałe, musimy podać w żądaniu ich globalną nazwę, która zawiera również nazwę komponentu. Pełną nazwę parametru zwraca metoda getParameterId().

let url = new URL({link //foo!});
url.searchParams.set({$control->getParameterId('bar')}, bar);

fetch(url, {
	headers: {'X-Requested-With': 'XMLHttpRequest'},
})

A metoda handle z odpowiadającymi parametrami w komponencie:

public function handleFoo(int $bar): void
{
}

Dalsza lektura