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
{
}