AJAX & snippety

W erze nowoczesnych aplikacji internetowych, gdzie funkcjonalność jest często rozdzielona między serwerem a przeglądarką, AJAX jest niezbędnym elementem łączącym. Jakie możliwości oferuje nam Nette Framework w tej dziedzinie?

  • wysyłanie fragmentów szablonu, tzw. snippetów
  • 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. Od presentera zależy, w jaki sposób zareaguje na żądanie – może zwrócić dane w formacie JSON, wysłać fragment kodu HTML, dokument XML itp.

Po stronie przeglądarki inicjujemy żądanie AJAX za pomocą funkcji fetch():

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

Po stronie serwera rozpoznajemy żądanie AJAX za pomocą metody $httpRequest->isAjax() usługi enkapsulującej żądanie HTTP. Do detekcji używa nagłówka HTTP X-Requested-With, dlatego ważne jest, aby go wysyłać. W ramach presentera można użyć metody $this->isAjax().

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

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

Jeśli planujesz odpowiedzieć za pomocą specjalnego szablonu przeznaczonego dla AJAX, możesz to zrobić w następujący sposób:

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

Snippety

Najpotężniejszym narzędziem oferowanym przez Nette do łączenia serwera z klientem są snippety. Dzięki nim można przekształcić zwykłą aplikację w aplikację AJAXową przy minimalnym wysiłku i kilku linijkach kodu. Jak to wszystko działa, demonstruje przykład Fifteen, którego kod znajdziesz na GitHubie.

Snippety, czyli fragmenty, umożliwiają aktualizację tylko części strony, zamiast ponownego ładowania całej strony. Jest to nie tylko szybsze i bardziej efektywne, ale także zapewnia bardziej komfortowe doświadczenie użytkownika. Snippety mogą przypominać Hotwire dla Ruby on Rails lub Symfony UX Turbo. Co ciekawe, Nette wprowadziło snippety już 14 lat wcześniej.

Jak działają snippety? Przy pierwszym załadowaniu strony (żądanie nie-AJAXowe) ładowana jest cała strona wraz ze wszystkimi snippetami. Kiedy użytkownik wchodzi w interakcję ze stroną (np. klika przycisk, wysyła formularz itp.), zamiast ładowania całej strony wywoływane jest żądanie AJAX. Kod w presenterze wykonuje akcję i decyduje, które snippety należy zaktualizować. Nette renderuje te snippety i wysyła je w formie tablicy w formacie JSON. Kod obsługujący w przeglądarce wstawia otrzymane snippety z powrotem na stronę. Przesyłany jest więc tylko kod zmienionych snippetów, co oszczędza przepustowość i przyspiesza ładowanie w porównaniu do przesyłania całej zawartości strony.

Naja

Do obsługi snippetów po stronie przeglądarki służy biblioteka Naja. Zainstaluj ją jako pakiet node.js (do użytku z aplikacjami Webpack, Rollup, Vite, Parcel i innymi):

npm install naja

…lub bezpośrednio wstaw do szablonu strony:

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

Najpierw należy zainicjować bibliotekę:

naja.initialize();

Aby zwykły link (sygnał) lub wysłanie formularza przekształcić w żądanie AJAX, wystarczy oznaczyć odpowiedni link, formularz lub przycisk klasą ajax:

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

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

lub

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

Przerysowanie snippetów

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

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

Nette pozwala na jeszcze dokładniejszą kontrolę tego, co ma zostać przerysowane. Wspomniana metoda może bowiem przyjmować jako argument nazwę snippetu. Można więc unieważnić (czytaj: wymusić przerysowanie) na poziomie części szablonu. Jeśli unieważniony zostanie cały komponent, przerysowany zostanie również każdy jego snippet:

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

Snippety w Latte

Używanie snippetów w Latte jest niezwykle proste. Aby zdefiniować część szablonu jako snippet, wystarczy otoczyć ją znacznikami {snippet} i {/snippet}:

{snippet header}
	<h1>Witaj ... </h1>
{/snippet}

Snippet tworzy w stronie HTML element <div> ze specjalnym wygenerowanym id. Podczas przerysowywania snippeta aktualizowana jest zawartość tego elementu. Dlatego konieczne jest, aby podczas pierwszego renderowania strony renderowane były również wszystkie snippety, nawet jeśli na początku mogą być puste.

Możesz również utworzyć snippet z innym elementem niż <div> za pomocą n:atrybutu:

<article n:snippet="header" class="foo bar">
	<h1>Witaj ... </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}

W ten sposób powstanie kilka snippetów item-0, item-1 itd. Gdybyśmy bezpośrednio unieważnili dynamiczny snippet (na przykład item-1), nic by się nie przerysowało. Powodem jest to, że snippety naprawdę działają jak wycinki i renderowane są tylko one same. Jednak w szablonie faktycznie nie ma żadnego snippeta o nazwie item-1. Powstaje on dopiero podczas wykonywania kodu wokół snippeta, czyli pętli foreach. Dlatego oznaczamy część szablonu, która ma zostać wykonana, za pomocą znacznika {snippetArea}:

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

I zlecamy przerysowanie zarówno samego snippeta, jak i całego nadrzędnego obszaru:

$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 szablonu za pomocą znacznika {include} wstawiamy inny szablon zawierający snippety, konieczne jest ponowne umieszczenie wstawionego szablonu w snippetArea i unieważnienie go 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żna tworzyć również w komponentach, a Nette będzie je automatycznie przerysowywać. Istnieje jednak pewne ograniczenie: do przerysowania snippetów wywoływana jest metoda render() bez parametrów. Oznacza to, że przekazywanie parametrów w szablonie nie będzie działać:

OK
{control productGrid}

nie będzie działać:
{control productGrid $arg, $arg}
{control productGrid:paginator}

Wysyłanie danych użytkownika

Wraz ze snippetami możesz wysłać klientowi dowolne inne dane. Wystarczy je zapisać w obiekcie payload:

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

Przekazywanie parametrów

Jeśli za pomocą żądania AJAX wysyłamy parametry do komponentu, czy to parametry sygnału, czy parametry persistentne, musimy w żądaniu podać 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 odpowiednimi parametrami w komponencie:

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