AJAX & Schnipsel

Im Zeitalter moderner Webanwendungen, bei denen sich die Funktionalität oft zwischen Server und Browser erstreckt, ist AJAX ein wesentliches Verbindungselement. Welche Möglichkeiten bietet das Nette Framework in diesem Bereich?

  • Senden von Teilen des Templates, sogenannte Snippets
  • Übergabe von Variablen zwischen PHP und JavaScript
  • Werkzeuge zur Fehlersuche bei AJAX-Anfragen

AJAX-Anfrage

Eine AJAX-Anfrage unterscheidet sich im Grunde nicht von einer klassischen HTTP-Anfrage. Ein Presenter wird mit bestimmten Parametern aufgerufen. Es liegt am Präsentator, wie er auf die Anfrage antwortet – er kann Daten im JSON-Format zurückgeben, einen Teil des HTML-Codes, ein XML-Dokument usw. senden.

Auf der Browserseite initiieren wir eine AJAX-Anfrage mit der Funktion fetch():

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

Auf der Serverseite wird eine AJAX-Anfrage durch die Methode $httpRequest->isAjax() des Dienstes erkannt , der die HTTP-Anfrage kapselt. Sie verwendet den HTTP-Header X-Requested-With und muss daher unbedingt gesendet werden. Innerhalb des Presenters können Sie die Methode $this->isAjax() verwenden.

Wenn Sie Daten im JSON-Format senden möchten, verwenden Sie die sendJson() Methode. Die Methode beendet auch die Aktivität des Presenters.

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

Wenn Sie vorhaben, mit einer speziellen Vorlage für AJAX zu antworten, können Sie dies wie folgt tun:

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

Schnipsel

Das mächtigste Werkzeug, das Nette für die Verbindung zwischen Server und Client bietet, sind Snippets. Mit ihnen lässt sich eine gewöhnliche Anwendung mit minimalem Aufwand und ein paar Zeilen Code in eine AJAX-Anwendung verwandeln. Das Fifteen-Beispiel zeigt, wie das Ganze funktioniert, und der Code ist auf GitHub zu finden.

Mit Snippets oder Clippings können Sie nur Teile der Seite aktualisieren, anstatt die gesamte Seite neu zu laden. Das ist schneller und effizienter und bietet zudem eine komfortablere Benutzererfahrung. Snippets erinnern Sie vielleicht an Hotwire für Ruby on Rails oder Symfony UX Turbo. Interessanterweise hat Nette Snippets schon 14 Jahre früher eingeführt.

Wie funktionieren Snippets? Wenn die Seite zum ersten Mal geladen wird (eine Nicht-AJAX-Anfrage), wird die gesamte Seite, einschließlich aller Snippets, geladen. Wenn der Benutzer mit der Seite interagiert (z. B. auf eine Schaltfläche klickt, ein Formular ausfüllt usw.), wird nicht die gesamte Seite geladen, sondern eine AJAX-Anfrage gestellt. Der Code im Presenter führt die Aktion aus und entscheidet, welche Snippets aktualisiert werden müssen. Nette rendert diese Schnipsel und sendet sie in Form eines JSON-Arrays. Der Verarbeitungscode im Browser fügt dann die empfangenen Snippets wieder in die Seite ein. Es wird also nur der Code der geänderten Snippets übertragen, was im Vergleich zur Übertragung des gesamten Seiteninhalts Bandbreite spart und das Laden beschleunigt.

Naja

Um Snippets auf der Browserseite zu verarbeiten, wird die Naja-Bibliothek verwendet. Installieren Sie sie als node.js-Paket (zur Verwendung mit Anwendungen wie Webpack, Rollup, Vite, Parcel und anderen):

npm install naja

… oder fügen Sie sie direkt in die Seitenvorlage ein:

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

Zunächst müssen Sie die Bibliothek initialisieren:

naja.initialize();

Um einen gewöhnlichen Link (Signal) oder eine Formularübermittlung zu einer AJAX-Anfrage zu machen, markieren Sie einfach den entsprechenden Link, das Formular oder die Schaltfläche mit der Klasse 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>

Schnipsel neu zeichnen

Jedes Objekt der Klasse Control (einschließlich des Presenters selbst) hält fest, ob Änderungen eingetreten sind, die ein erneutes Zeichnen erforderlich machen. Zu diesem Zweck wird die Methode redrawControl() verwendet.

public function handleLogin(string $user): void
{
	// nach dem Einloggen muss der entsprechende Teil neu gezeichnet werden
	$this->redrawControl();
	//...
}

Nette ermöglicht auch eine genauere Kontrolle darüber, was neu gezeichnet werden muss. Die oben erwähnte Methode kann den Snippet-Namen als Argument verwenden. So ist es möglich, auf der Ebene des Schablonenteils zu invalidieren (d.h. ein Neuzeichnen zu erzwingen). Wird die gesamte Komponente für ungültig erklärt, wird auch jedes Snippet davon neu gezeichnet:

// macht den "Header"-Schnipsel ungültig
$this->redrawControl('header');

Schnipsel in Latte

Die Verwendung von Snippets in Latte ist extrem einfach. Um einen Teil der Vorlage als Snippet zu definieren, umhüllen Sie ihn einfach mit den Tags {snippet} und {/snippet}:

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

Das Snippet erzeugt ein Element <div> in der HTML-Seite mit einem speziell generierten id. Wenn ein Snippet neu gezeichnet wird, wird der Inhalt dieses Elements aktualisiert. Daher müssen beim ersten Rendering der Seite alle Snippets ebenfalls gerendert werden, auch wenn sie zunächst leer sind.

Sie können auch ein Snippet mit einem anderen Element als <div> mit einem n:-Attribut erstellen:

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

Schnipsel Bereiche

Snippet-Namen können auch Ausdrücke sein:

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

Auf diese Weise erhalten wir mehrere Snippets wie item-0, item-1, usw. Wenn wir einen dynamischen Ausschnitt (z. B. item-1) direkt ungültig machen würden, würde nichts neu gezeichnet werden. Der Grund dafür ist, dass Snippets als echte Auszüge funktionieren und nur sie selbst direkt gerendert werden. In der Vorlage gibt es jedoch technisch gesehen kein Snippet namens item-1. Es taucht nur auf, wenn der umgebende Code des Snippets ausgeführt wird, in diesem Fall die foreach-Schleife. Daher markieren wir den Teil der Vorlage, 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 werden sowohl das einzelne Snippet als auch den gesamten übergreifenden Bereich neu zeichnen:

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

Es ist auch wichtig, dass das Array $items nur die Elemente enthält, die neu gezeichnet werden sollen.

Beim Einfügen einer anderen Vorlage in die Hauptvorlage unter Verwendung des {include} -Tags, das Snippets enthält, muss die eingefügte Vorlage erneut in ein snippetArea -Tag eingeschlossen und sowohl das Snippet als auch der Bereich gemeinsam ungültig gemacht werden:

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

Schnipsel in Komponenten

Sie können Snippets in Komponenten erstellen, und Nette wird sie automatisch neu zeichnen. Es gibt jedoch eine bestimmte Einschränkung: Um Snippets neu zu zeichnen, wird die Methode render() ohne Parameter aufgerufen. Die Übergabe von Parametern in der Vorlage funktioniert also nicht:

OK
{control productGrid}

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

Senden von Benutzerdaten

Neben den Snippets können Sie beliebige weitere Daten an den Client senden. Schreiben Sie sie einfach in das payload Objekt:

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

Senden von Parametern

Wenn wir Parameter über eine AJAX-Anfrage an die Komponente senden, egal ob es sich um Signalparameter oder dauerhafte Parameter handelt, müssen wir ihren globalen Namen angeben, der auch den Namen der Komponente enthält. Den vollständigen Namen des Parameters gibt die Methode getParameterId() zurück.

let url = new URL({link //foo!});
url.searchParams.set({$control->getParameterId('bar')}, bar);

fetch(url, {
	headers: {'X-Requested-With': 'XMLHttpRequest'},
})

Eine Handle-Methode mit den entsprechenden Parametern in der Komponente:

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