Nette Documentation Preview

syntax
AJAX & Snippets
***************

<div class=perex>

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

</div>


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()`:

```js
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 |http:request]. 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()` |presenters#Eine Antwort senden]. Die Methode beendet zugleich die Tätigkeit des Presenters.

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

Wenn Sie mit einem speziellen, für AJAX gedachten Template antworten wollen, geht das so:

```php
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 |https://github.com/nette-examples/fifteen] 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 |https://naja.js.org]. [Installieren Sie sie |https://naja.js.org/#/guide/01-install-setup-naja] als Node.js-Paket (für die Verwendung mit Bundlern wie Webpack, Rollup, Vite, Parcel und anderen):

```shell
npm install naja
```

…oder binden Sie sie direkt in das Template der Seite ein:

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

Zuerst müssen Sie die Bibliothek [initialisieren |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization]:

```js
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:

```latte
<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 |components] (einschließlich des Presenters selbst) merkt sich, ob Änderungen eingetreten sind, die ein Neuzeichnen erfordern. Dazu dient die Methode `redrawControl()`:

```php
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:

```php
// 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}`:

```latte
{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:

```latte
<article n:snippet="header" class="foo bar">
	<h1>Hallo ... </h1>
</article>
```


Snippet-Bereiche
----------------

Namen von Snippets können auch Ausdrücke sein:

```latte
{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}`:

```latte
<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:

```php
$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:

```latte
{snippetArea include}
	{include 'included.latte'}
{/snippetArea}
```

```latte
{* included.latte *}
{snippet item}
	...
{/snippet}
```

```php
$this->redrawControl('include');
$this->redrawControl('item');
```


Snippets in Komponenten
-----------------------

Snippets können Sie auch in [Komponenten|components] 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:

```latte
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`:

```php
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()`.

```js
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:

```php
public function handleFoo(int $bar): void
{
}
```


Weiterführende Lektüre
======================

- [Dynamische Snippets |best-practices:dynamic-snippets]

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

Weiterführende Lektüre