AJAX & Snippets
Im Zeitalter moderner Webanwendungen, in dem die Funktionalität oft zwischen Server und Browser aufgeteilt ist, ist AJAX das unverzichtbare Bindeglied. Welche Möglichkeiten bietet uns das Nette Framework in diesem Bereich?
- Senden von Teilen des Templates, sogenannten Snippets
- Übergabe von Variablen zwischen PHP und JavaScript
- Werkzeuge zum Debuggen von AJAX-Requests
AJAX-Request
Ein AJAX-Request unterscheidet sich grundsätzlich nicht von einem klassischen HTTP-Request. Es wird ein Presenter mit bestimmten Parametern aufgerufen. Es liegt am Presenter, wie er auf den Request antwortet – er kann Daten im JSON-Format zurückgeben, ein Stück HTML-Code senden, ein XML-Dokument usw.
Auf der Browserseite starten wir einen AJAX-Request mit der Funktion fetch():
fetch(url, {
headers: {'X-Requested-With': 'XMLHttpRequest'},
})
.then(response => response.json())
.then(payload => {
// die Antwort verarbeiten
});
Auf der Serverseite wird ein AJAX-Request an der Methode $httpRequest->isAjax() des Services erkannt, der den HTTP-Request kapselt. Zur Erkennung dient der HTTP-Header
X-Requested-With, es ist daher wesentlich, ihn mitzusenden. Innerhalb des Presenters können Sie die Methode
$this->isAjax() verwenden.
Wenn Sie Daten im JSON-Format senden wollen, verwenden Sie die Methode sendJson(). Die Methode beendet
zugleich die Tätigkeit des Presenters.
public function actionExport(): void
{
$this->sendJson($this->model->getData());
}
Wenn Sie mit einem speziellen, für AJAX gedachten Template antworten wollen, geht das so:
public function handleClick($param): void
{
if ($this->isAjax()) {
$this->template->setFile('path/to/ajax.latte');
}
// ...
}
Snippets
Das mächtigste Werkzeug, das Nette für die Verbindung von Server und Client bietet, sind Snippets. Mit ihnen verwandeln Sie eine gewöhnliche Anwendung mit minimalem Aufwand und wenigen Zeilen Code in eine AJAX-Anwendung. Wie das alles funktioniert, zeigt das Beispiel Fifteen, dessen Code Sie auf GitHub finden.
Snippets erlauben es, nur Teile der Seite zu aktualisieren, statt die ganze Seite neu zu laden. Das ist nicht nur schneller und effizienter, sondern bietet auch ein angenehmeres Benutzererlebnis. Snippets erinnern Sie vielleicht an Hotwire für Ruby on Rails oder Symfony UX Turbo. Interessanterweise hat Nette die Snippets 14 Jahre früher eingeführt.
Wie funktionieren Snippets? Beim ersten Laden der Seite (einem Nicht-AJAX-Request) wird die gesamte Seite samt allen Snippets
geladen. Wenn der Benutzer mit der Seite interagiert (z. B. auf eine Schaltfläche klickt, ein Formular absendet usw.), wird statt
des Neuladens der ganzen Seite ein AJAX-Request ausgelöst. Der Code im Presenter führt die Aktion aus und entscheidet, welche
Snippets aktualisiert werden müssen. Nette rendert diese Snippets und sendet sie als JSON-Payload, das ein Array mit den Snippets
enthält. Der Code im Browser fügt die empfangenen Snippets anschließend wieder in die Seite ein. Es wird also nur der Code der
geänderten Snippets übertragen, was Bandbreite spart und das Laden gegenüber der Übertragung des gesamten Seiteninhalts
beschleunigt. Wird kein Snippet mit redrawControl() invalidiert, gibt Nette auch bei einem AJAX-Request die gesamte
Seite zurück – Snippets werden nur dann gesendet, wenn etwas invalidiert wurde.
Naja
Für die Arbeit mit Snippets auf der Browserseite dient die Bibliothek Naja. Installieren Sie sie als Node.js-Paket (für die Verwendung mit Bundlern wie Webpack, Rollup, Vite, Parcel und anderen):
npm install naja
…oder binden Sie sie direkt in das Template der Seite ein:
<script src="https://unpkg.com/naja@3/dist/Naja.min.js"></script>
Zuerst müssen Sie die Bibliothek initialisieren:
naja.initialize();
Um aus einem gewöhnlichen Link (Signal) oder dem Absenden eines Formulars einen AJAX-Request zu machen, genügt es, den
betreffenden Link, das Formular oder die Schaltfläche mit der Klasse ajax zu kennzeichnen:
<a n:href="go!" class="ajax">Los</a>
<form n:name="form" class="ajax">
<input n:name="submit">
</form>
oder
<form n:name="form">
<input n:name="submit" class="ajax">
</form>
Snippets neu zeichnen
Jedes Objekt der Klasse Control (einschließlich des Presenters
selbst) merkt sich, ob Änderungen eingetreten sind, die ein Neuzeichnen erfordern. Dazu dient die Methode
redrawControl():
public function handleLogin(string $user): void
{
// nach dem Login muss der betreffende Teil neu gezeichnet werden
$this->redrawControl();
// ...
}
Nette erlaubt eine noch feinere Steuerung dessen, was neu gezeichnet werden soll. Die Methode kann als Argument den Namen des Snippets entgegennehmen. Es lässt sich also auf der Ebene von Template-Teilen invalidieren (das heißt: ein Neuzeichnen erzwingen). Wird die gesamte Komponente invalidiert, wird auch jedes Snippet darin neu gezeichnet:
// invalidiert das Snippet 'header'
$this->redrawControl('header');
Eine anstehende Invalidierung lässt sich über den zweiten Parameter $redraw auch aufheben: Der Aufruf
$this->redrawControl('header', redraw: false) markiert das Snippet als nicht neu zu zeichnen. Die vollständige
Signatur lautet redrawControl(?string $snippet = null, bool $redraw = true).
Snippets in Latte
Die Verwendung von Snippets in Latte ist ausgesprochen einfach. Um einen Teil des Templates als Snippet zu definieren,
umschließen Sie ihn einfach mit den Tags {snippet} und {/snippet}:
{snippet header}
<h1>Hallo ... </h1>
{/snippet}
Das Snippet erzeugt in der HTML-Seite ein Element <div> mit einer speziell generierten id. Beim
Neuzeichnen des Snippets wird der Inhalt dieses Elements aktualisiert. Deshalb ist es nötig, dass beim ersten Rendern der Seite
auch alle Snippets gerendert werden, selbst wenn sie anfangs leer sein sollten.
Ein Snippet lässt sich mit einem n:Attribut auch mit einem anderen Element als <div> erzeugen:
<article n:snippet="header" class="foo bar">
<h1>Hallo ... </h1>
</article>
Snippet-Bereiche
Namen von Snippets können auch Ausdrücke sein:
{foreach $items as $id => $item}
<li n:snippet="item-{$id}">{$item}</li>
{/foreach}
Für sich genommen ist das ein nicht funktionsfähiger Zwischenschritt: Wird ein dynamisches Snippet außerhalb eines
statischen {snippet} oder {snippetArea} gerendert, löst es ein E_USER_WARNING mit der
Meldung Dynamic snippets are allowed only inside static snippet/snippetArea. aus. Das beheben wir gleich.
So entstehen mehrere Snippets wie item-0, item-1 usw. Würden wir ein dynamisches Snippet (z. B.
item-1) direkt invalidieren, würde nichts neu gezeichnet. Der Grund ist, dass Snippets wirklich als Ausschnitte
funktionieren und nur sie selbst direkt gerendert werden. Im Template gibt es jedoch technisch gesehen gar kein Snippet namens
item-1. Es entsteht erst, wenn der Code rund um das Snippet, also die foreach-Schleife, ausgeführt wird. Deshalb
kennzeichnen wir den Teil des Templates, der ausgeführt werden muss, mit dem Tag {snippetArea}:
<ul n:snippetArea="itemsContainer">
{foreach $items as $id => $item}
<li n:snippet="item-{$id}">{$item}</li>
{/foreach}
</ul>
Und wir fordern das Neuzeichnen sowohl des einzelnen Snippets als auch des gesamten übergeordneten Bereichs an:
$this->redrawControl('itemsContainer');
$this->redrawControl('item-1');
Zugleich ist es ratsam, dafür zu sorgen, dass das Array $items nur die Einträge enthält, die neu gezeichnet
werden sollen.
Binden wir mit dem Tag {include} ein weiteres Template mit Snippets in das Haupttemplate ein, ist es nötig, das
eingebundene Template erneut in eine snippetArea zu hüllen und diese zusammen mit dem Snippet zu invalidieren:
{snippetArea include}
{include 'included.latte'}
{/snippetArea}
{* included.latte *}
{snippet item}
...
{/snippet}
$this->redrawControl('include');
$this->redrawControl('item');
Snippets in Komponenten
Snippets können Sie auch in Komponenten erstellen, und Nette
zeichnet sie automatisch neu. Es gibt jedoch eine Einschränkung: Zum Neuzeichnen von Snippets ruft Nette die Methode
render() ohne Parameter auf. Die Übergabe von Parametern im Template funktioniert daher nicht:
OK
{control productGrid}
funktioniert nicht:
{control productGrid $arg, $arg}
{control productGrid:paginator}
Eigene Daten senden
Zusammen mit den Snippets können Sie dem Client beliebige weitere Daten senden. Schreiben Sie sie einfach in das Objekt
payload:
public function actionDelete(int $id): void
{
// ...
if ($this->isAjax()) {
$this->payload->message = 'Erfolg';
}
}
Weiterleitung
Während eines AJAX-Requests senden die Methoden redirect() und redirectUrl() keine
HTTP-Weiterleitung. Stattdessen schreiben sie die Ziel-URL in das Payload (das im AJAX-Response gesendete Datenobjekt), konkret in
dessen Eigenschaft payload.redirect, und senden es; die eigentliche Weiterleitung führt dann die clientseitige
Bibliothek (Naja) aus.
Parameter übergeben
Wenn wir einer Komponente über einen AJAX-Request Parameter senden, seien es Signal- oder persistente Parameter, müssen wir
im Request ihren globalen Namen angeben, der auch den Namen der Komponente enthält. Den vollständigen Parameternamen liefert die
Methode getParameterId().
let url = new URL({link //foo!});
url.searchParams.set({$control->getParameterId('bar')}, bar);
fetch(url, {
headers: {'X-Requested-With': 'XMLHttpRequest'},
})
Und die handle-Methode mit den entsprechenden Parametern in der Komponente:
public function handleFoo(int $bar): void
{
}