AJAX i fragmenty

W dobie nowoczesnych aplikacji internetowych, w których funkcjonalność często rozciąga się między serwerem a przeglądarką, AJAX jest niezbędnym elementem łączącym. Jakie możliwości w tym zakresie oferuje Nette Framework?

  • wysyłanie części szablonu, tzw. snippetów
  • przekazywanie zmiennych między PHP i JavaScript
  • narzędzia do debugowania żądań AJAX

Żądanie AJAX

Żądanie AJAX zasadniczo nie różni się od klasycznego żądania HTTP. Prezenter jest wywoływany z określonymi parametrami. To od prezentera zależy, jak odpowie 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 żądanie AJAX jest rozpoznawane przez metodę $httpRequest->isAjax() usługi hermetyzującej żądanie HTTP. Używa ona nagłówka HTTP X-Requested-With, więc jest niezbędna do jego wysłania. W prezenterze można użyć metody $this->isAjax().

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

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

Jeśli planujesz odpowiedzieć za pomocą specjalnego szablonu zaprojektowanego 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');
	}
	//...
}

Fragmenty

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ę AJAX przy minimalnym wysiłku i kilku linijkach kodu. Przykład Fifteen pokazuje, jak to wszystko działa, a jego kod można znaleźć na GitHubie.

Snippety lub wycinki pozwalają aktualizować tylko części strony, zamiast przeładowywać całą stronę. Jest to szybsze i bardziej wydajne, a także zapewnia wygodniejsze wrażenia użytkownika. Snippety mogą przypominać Hotwire dla Ruby on Rails lub Symfony UX Turbo. Co ciekawe, Nette wprowadziło snippety 14 lat wcześniej.

Jak działają snippety? Kiedy strona jest ładowana po raz pierwszy (żądanie inne niż AJAX), ładowana jest cała strona, w tym wszystkie snippety. Gdy użytkownik wchodzi w interakcję ze stroną (np. klika przycisk, przesyła formularz itp.), zamiast ładowania całej strony, wykonywane jest żądanie AJAX. Kod w prezenterze wykonuje akcję i decyduje, które fragmenty wymagają aktualizacji. Nette renderuje te fragmenty i wysyła je w postaci tablicy JSON. Kod obsługi w przeglądarce wstawia następnie otrzymane fragmenty z powrotem na stronę. W związku z tym przesyłany jest tylko kod zmienionych fragmentó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 używana jest biblioteka Na ja. Należy ją zainstal ować jako pakiet node.js (do użytku z aplikacjami takimi jak Webpack, Rollup, Vite, Parcel i innymi):

npm install naja

… lub wstawić ją bezpośrednio do szablonu strony:

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

Najpierw należy zainicjować bibliotekę:

naja.initialize();

Aby uczynić zwykły link (sygnał) lub przesłanie formularza żądaniem 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>

or

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

Przerysowywanie fragmentów

Każdy obiekt klasy Control (w tym sam Presenter) przechowuje informacje o tym, czy wystąpiły zmiany, które wymagają jego przerysowania. W tym celu wykorzystywana jest metoda redrawControl().

public function handleLogin(string $user): void
{
	// po zalogowaniu konieczne jest przerysowanie odpowiedniego fragmentu
	$this->redrawControl();
	//...
}

Nette pozwala również na dokładniejszą kontrolę tego, co wymaga przerysowania. Wspomniana metoda może przyjąć nazwę fragmentu jako argument. W ten sposób możliwe jest unieważnienie (czyli wymuszenie przerysowania) na poziomie części szablonu. Jeśli cały komponent zostanie unieważniony, każdy jego fragment zostanie również przerysowany:

// unieważnia fragment "nagłówka
$this->redrawControl('header');

Snippety w Latte

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

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

Snippet tworzy element <div> na stronie HTML ze specjalnie wygenerowanym id. Podczas przerysowywania fragmentu zawartość tego elementu jest aktualizowana. Dlatego też, gdy strona jest początkowo renderowana, wszystkie snippety muszą być również renderowane, nawet jeśli początkowo mogą być puste.

Można również utworzyć snippet z elementem innym niż <div> używając atrybutu n:attribute:

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

Obszary wycinków

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

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

W ten sposób otrzymamy kilka snippetów, takich jak item-0, item-1, itd. Gdybyśmy bezpośrednio unieważnili dynamiczny snippet (np. item-1), nic nie zostałoby przerysowane. Powodem jest to, że snippety działają jako prawdziwe fragmenty i tylko one są renderowane bezpośrednio. Jednak w szablonie nie ma technicznie snippetu o nazwie item-1. Pojawia się on dopiero podczas wykonywania otaczającego kodu snippetu, w tym przypadku pętli foreach. Dlatego część szablonu, która musi zostać wykonana, oznaczymy tagiem {snippetArea}:

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

I przerysujemy zarówno pojedynczy snippet, jak i cały obszar nadrzędny:

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

Ważne jest również, aby upewnić się, że tablica $items zawiera tylko elementy, które powinny zostać przerysowane.

Podczas wstawiania innego szablonu do szablonu głównego za pomocą znacznika {include}, który zawiera snippety, konieczne jest ponowne zawinięcie dołączonego szablonu w snippetArea i unieważnienie zarówno snippetu, jak i obszaru razem:

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

Snippety w komponentach

Możesz tworzyć fragmenty w komponentach, a Nette automatycznie je przerysuje. Istnieje jednak pewne ograniczenie: aby przerysować snippety, Nette wywołuje metodę render() bez żadnych parametrów. Zatem przekazywanie parametrów w szablonie nie będzie działać:

OK
{control productGrid}

will not work:
{control productGrid $arg, $arg}
{control productGrid:paginator}

Wysyłanie danych użytkownika

Wraz ze snippetami można wysyłać do klienta dowolne dodatkowe dane. Wystarczy zapisać je w obiekcie payload:

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

Wysyłanie parametrów

Gdy wysyłamy parametry do komponentu za pośrednictwem żądania AJAX, niezależnie od tego, czy są to parametry sygnału, czy parametry trwałe, musimy 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'},
})

Metoda uchwytu z odpowiednimi parametrami w komponencie:

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