AJAX i snippety

Nowoczesne aplikacje internetowe działają dziś w połowie na serwerze, w połowie w przeglądarce. AJAX jest kluczowym elementem łączącym. Jakie wsparcie oferuje Nette Framework?

  • Wysyłanie fragmentów szablonów
  • przekazywanie zmiennych między PHP a JavaScriptem
  • debugowanie aplikacji AJAX

Żądanie AJAX

Żądanie AJAX nie różni się od klasycznego żądania – prezenter jest wywoływany z określonym widokiem i parametrami. Również od prezentera zależy, jak na nie odpowie: może użyć własnej procedury, która zwraca fragment kodu HTML (HTML snippet), dokument XML, obiekt JSON lub kod JavaScript.

Po stronie serwera żądanie AJAX może zostać wykryte za pomocą metody serwisowej obudowującej żądanie HTTP $httpRequest->isAjax() (wykrywa na podstawie nagłówka HTTP X-Requested-With). Wewnątrz prezentera dostępny jest skrót w postaci metody $this->isAjax().

Aby wysłać dane do przeglądarki w formacie JSON, możesz użyć gotowego obiektu payload:

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

Jeśli potrzebujesz pełnej kontroli nad wysłanym JSON, użyj metody sendJson w prezenterze. Spowoduje to natychmiastowe zakończenie prezentera i zrezygnowanie z szablonu:

$this->sendJson(['key' => 'value', /* ... */]);

Jeśli chcemy wysłać HTML, możemy wybrać specjalny szablon dla AJAX:

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

Naja

Biblioteka Naja służy do obsługi żądań AJAX po stronie przeglądarki. Zainstaluj go jako pakiet node.js (do użytku z Webpack, Rollup, Vite, Parcel i innych):

npm install naja

…lub osadzić bezpośrednio w szablonie strony:

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

Aby utworzyć żądanie AJAX ze zwykłego linku (sygnału) lub submitu formularza, 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>

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

Snippets

Znacznie potężniejszym narzędziem jest wbudowana obsługa snippetów AJAX. Dzięki niemu można zamienić zwykłą aplikację w AJAXową za pomocą zaledwie kilku linijek kodu. Przykład Fifteen, którego kod można znaleźć na GitHubie, demonstruje jak to działa.

Działanie snippetów polega na tym, że na początkowym (czyli nie-AJAX-owym) żądaniu przenoszona jest cała strona, a następnie na każdym podżądaniu AJAX-owym (= żądanie do tego samego prezentera i widoku) przenoszony jest tylko kod zmienionych fragmentów we wspomnianym repozytorium payload. Istnieją dwa mechanizmy tego działania: unieważnianie i renderowanie snippetów.

Snippets mogą przypominać Hotwire dla Ruby on Rails lub Symfony UX Turbo, ale Nette wymyślił je czternaście lat wcześniej.

Unieważnianie fragmentów

Każdy obiekt klasy Control (którą jest sam Presenter) potrafi zapamiętać, czy w sygnale zaszły zmiany wymagające jego przerysowania. Służy do tego para metod redrawControl() i isControlInvalid(). Przykład:

public function handleLogin(string $user): void
{
	// po zalogowaniu się użytkownika obiekt musi zostać przerysowany
	$this->redrawControl();
	// ...
}

Nette oferuje jednak jeszcze dokładniejszą rozdzielczość niż na poziomie komponentów. Metody te mogą przyjąć jako argument nazwę “snippet”, czyli wycinka. Możliwe jest więc unieważnienie (czyli wymuszenie przerysowania) na poziomie tych wycinków (każdy obiekt może mieć dowolną liczbę wycinków). Jeśli cały komponent zostanie unieważniony, każdy wycinek zostanie przerysowany. Komponent jest “unieważniony” nawet jeśli podkomponent jest unieważniony.

$this->isControlInvalid(); // -> false

$this->redrawControl('header'); // unieważnia snippet 'header'
$this->isControlInvalid('header'); // -> true
$this->isControlInvalid('footer'); // -> false
$this->isControlInvalid(); // -> prawda, przynajmniej jeden fragment jest nieprawidłowy

$this->redrawControl(); // unieważnia cały komponent, każdy fragment
$this->isControlInvalid('footer'); // -> true

Komponent, który otrzymuje sygnał, jest automatycznie oznaczany jako wyłączony.

Unieważniając snippety, wiemy dokładnie, które części których elementów będą musiały zostać przerysowane.

Tagi {snippet} … {/snippet}

Renderowanie strony jest bardzo podobne do normalnego żądania: ładowane są te same szablony itp. Ważne jest jednak pominięcie części, które nie powinny być wyprowadzane; pozostałe części są przypisywane do identyfikatora i wysyłane do użytkownika w formacie zrozumiałym dla JavaScript handler.

Składnia

Jeśli wewnątrz szablonu znajduje się kontrolka lub snippet, musimy owinąć go znacznikiem {snippet} ... {/snippet} para – zapewniają one wycięcie wyrenderowanego snippetu i wysłanie go do przeglądarki. Zawija go również za pomocą tagu pomocniczego <div> z wygenerowanym id. W powyższym przykładzie snippet nosi nazwę header i może również reprezentować np. szablon kontrolny:

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

Fragment o typie innym niż <div> lub snippet z dodatkowymi atrybutami HTML uzyskuje się poprzez zastosowanie wariantu atrybutów:

<article n:snippet="header" class="foo bar">
	<h1>Hello ... </h1>
</article>

Dynamiczne fragmenty

Nette pozwala również na stosowanie snippetów, których nazwa jest tworzona w czasie biegu – czyli dynamicznie. Jest to przydatne w przypadku różnych list, gdzie przy zmianie jednego wiersza nie chcemy AJAXować całej listy, a jedynie sam wiersz. Przykład:

<ul n:snippet="itemsContainer">
	{foreach $list as $id => $item}
	<li n:snippet="item-$id">{$item} <a class="ajax" n:href="update! $id">update</a></li>
	{/foreach}
</ul>

Mamy tu statyczny snippet itemsContainer, zawierający kilka dynamicznych snippetów item-0, item-1 itd.

Nie można bezpośrednio unieważnić dynamicznych snippetów (unieważnienie item-1 nic nie daje), trzeba unieważnić ich nadrzędny statyczny snippet (tutaj snippet itemsContainer). Wówczas cały kod kontenera zostanie wykonany, ale do przeglądarki zostaną wysłane tylko jego podkontenerowe snippety. Jeśli chcesz, aby przeglądarka otrzymała tylko jeden z nich, musisz zmodyfikować dane wejściowe tego kontenera, aby nie generował pozostałych.

W powyższym przykładzie musisz po prostu upewnić się, że gdy wykonasz żądanie ajaxowe, w zmiennej $list znajduje się tylko jeden wpis, a zatem, że pętla foreach wypełnia tylko jeden dynamiczny snippet:

class HomePresenter extends Nette\Application\UI\Presenter
{
	/**
	 * Tato metoda vrací data pro seznam.
	 * Obvykle se jedná pouze o vyžádání dat z modelu.
	 * Pro účely tohoto příkladu jsou data zadána natvrdo.
	 */
	private function getTheWholeList(): array
	{
		return [
			'První',
			'Druhý',
			'Třetí',
		];
	}

	public function renderDefault(): void
	{
		if (!isset($this->template->list)) {
			$this->template->list = $this->getTheWholeList();
		}
	}

	public function handleUpdate(int $id): void
	{
		$this->template->list = $this->isAjax()
				? []
				: $this->getTheWholeList();
		$this->template->list[$id] = 'Updated item';
		$this->redrawControl('itemsContainer');
	}
}

Snippety w dołączonym szablonie

Może się zdarzyć, że w jakimś szablonie mamy snippet, który dopiero chcemy włączyć do innego szablonu. W tym przypadku musimy owinąć osadzenie tego szablonu znacznikami snippetArea, które następnie unieważniamy wraz z samym snippetem.

Znaczniki snippetArea gwarantują, że kod osadzający szablon zostanie wykonany, ale do przeglądarki zostanie wysłany tylko wycinek z osadzanego szablonu.

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

Takie podejście może być również stosowane w połączeniu z dynamicznymi snippetami.

Dodawanie i usuwanie

Jeśli dodasz nowy element i unieważnisz itemsContainer, to żądanie AJAX zwróci również nowy snippet, ale handler javascript nie może go nigdzie przypisać. W rzeczywistości na stronie nie ma jeszcze elementu HTML o tym identyfikatorze.

W takim przypadku najłatwiej jest owinąć całą listę jeszcze jednym snippetem i unieważnić całość:

{snippet wholeList}
<ul n:snippet="itemsContainer">
	{foreach $list as $id => $item}
	<li n:snippet="item-$id">{$item} <a class="ajax" n:href="update! $id">update</a></li>
	{/foreach}
</ul>
{/snippet}
<a class="ajax" n:href="add!">Add</a>
public function handleAdd(): void
{
	$this->template->list = $this->getTheWholeList();
	$this->template->list[] = 'New one';
	$this->redrawControl('wholeList');
}

To samo tyczy się usuwania. Można by jakoś wysłać pusty snippet, ale w praktyce większość list jest paginowana i byłoby to zbyt skomplikowane, aby bardziej ekonomicznie usunąć jeden plus ewentualnie załadować inny (który nie pasował wcześniej).

Wysyłanie parametrów do komponentu

Jeśli wysyłamy parametry do komponentu za pomocą żądania AJAX, zarówno parametry sygnałowe, jak i parametry trwałe, musimy określić ich globalną nazwę w żądaniu, które zawiera nazwę komponentu. Pełna nazwa parametru jest zwracana przez metodę getParameterId().

$.getJSON(
	{link changeCountBasket!},
	{
		{$control->getParameterId('id')}: id,
		{$control->getParameterId('count')}: count
	}
});

Metoda handle z odpowiednimi parametrami w komponencie.

public function handleChangeCountBasket(int $id, int $count): void
{

}
wersja: 4.0