AJAX & Schnipsel
Moderne Webanwendungen laufen heute zur Hälfte auf einem Server und zur Hälfte in einem Browser. AJAX ist ein wichtiger verbindender Faktor. Welche Unterstützung bietet das Nette Framework?
- Senden von Template-Fragmenten (sogenannte snippets)
- Übergabe von Variablen zwischen PHP und JavaScript
- Debugging von AJAX-Anwendungen
AJAX-Anfrage
Eine AJAX-Anfrage unterscheidet sich nicht von einer klassischen Anfrage – der Presenter wird mit einer bestimmten Ansicht und Parametern aufgerufen. Es liegt auch am Präsentator, wie er darauf reagiert: Er kann seine eigene Routine verwenden, die ein HTML-Codefragment (HTML-Snippet), ein XML-Dokument, ein JSON-Objekt oder JavaScript-Code zurückgibt.
Auf der Serverseite kann eine AJAX-Anfrage mit Hilfe der Servicemethode erkannt werden , die die HTTP-Anfrage kapselt $httpRequest->isAjax()
(erkennt
auf der Grundlage des HTTP-Headers X-Requested-With
). Innerhalb des Presenters ist eine Abkürzung in Form der
Methode $this->isAjax()
verfügbar.
Es gibt ein vorverarbeitetes Objekt namens payload
, das für das Senden von Daten in JSON an den Browser
bestimmt ist.
public function actionDelete(int $id): void
{
if ($this->isAjax()) {
$this->payload->message = 'Erfolg';
}
// ...
}
Um die volle Kontrolle über Ihre JSON-Ausgabe zu haben, verwenden Sie die Methode sendJson
in Ihrem Presenter.
Sie beendet den Presenter sofort und Sie können auf eine Vorlage verzichten:
$this->sendJson(['key' => 'value', /* ... */]);
Wenn wir HTML senden wollen, können wir entweder eine spezielle Vorlage für AJAX-Anfragen festlegen:
public function handleClick($param): void
{
if ($this->isAjax()) {
$this->template->setFile('path/to/ajax.latte');
}
// ...
}
Naja
Die Naja-Bibliothek wird verwendet, um AJAX-Anfragen auf der Browserseite zu verarbeiten. Installieren Sie sie als node.js-Paket (zur Verwendung mit Webpack, Rollup, Vite, Parcel und mehr):
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>
Um eine AJAX-Anfrage aus einem regulären Link (Signal) oder einer Formularübermittlung zu erzeugen, 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
Es gibt ein weitaus mächtigeres Werkzeug der eingebauten AJAX-Unterstützung – Snippets. Mit ihnen ist es möglich, eine normale Anwendung mit nur wenigen Zeilen Code in eine AJAX-Anwendung zu verwandeln. Wie das Ganze funktioniert, wird im Fifteen-Beispiel gezeigt, dessen Code auch im Build oder auf GitHub verfügbar ist.
Die Funktionsweise der Snippets besteht darin, dass bei der ersten (d.h. Nicht-AJAX-) Anfrage die gesamte Seite übertragen
wird und dann bei jeder AJAX-Unteranfrage (Anfrage
derselben Ansicht desselben Präsentators) nur der Code der geänderten Teile in das bereits erwähnte Repository
payload
übertragen wird.
Snippets erinnern vielleicht an Hotwire für Ruby on Rails oder Symfony UX Turbo, aber Nette hat sie schon vierzehn Jahre früher erfunden.
Invalidierung von Snippets
Jeder Nachkomme der Klasse Control (also auch ein Presenter) ist
in der Lage, sich zu merken, ob es während einer Anfrage Änderungen gab, die ein erneutes Rendern erforderlich machen. Dafür
gibt es zwei Methoden: redrawControl()
und isControlInvalid()
. Ein Beispiel:
public function handleLogin(string $user): void
{
// Das Objekt muss neu gerendert werden, nachdem sich der Benutzer angemeldet hat
$this->redrawControl();
// ...
}
Nette bietet jedoch eine noch feinere Auflösung als ganze Komponenten. Die aufgeführten Methoden akzeptieren den Namen eines sogenannten “Snippets” als optionalen Parameter. Ein “Snippet” ist im Grunde ein Element in Ihrer Vorlage, das zu diesem Zweck durch ein Latte-Makro markiert wurde, mehr dazu später. So ist es möglich, eine Komponente aufzufordern, nur Teile ihrer Vorlage neu zu zeichnen. Wenn die gesamte Komponente ungültig ist, werden alle ihre Schnipsel neu gerendert. Eine Komponente ist auch dann “ungültig”, wenn eine ihrer Unterkomponenten ungültig ist.
$this->isControlInvalid(); // -> false
$this->redrawControl('header'); // macht das Snippet namens 'header' ungültig
$this->isControlInvalid('header'); // -> true
$this->isControlInvalid('footer'); // -> false
$this->isControlInvalid(); // -> true, mindestens ein Snippet ist ungültig
$this->redrawControl(); // macht die gesamte Komponente ungültig, jedes Snippet
$this->isControlInvalid('footer'); // -> true
Eine Komponente, die ein Signal erhält, wird automatisch zum Neuzeichnen markiert.
Dank des Snippet-Redrawing wissen wir genau, welche Teile welcher Elemente neu gezeichnet werden sollten.
Tag {snippet} … {/snippet}
Das Rendering der Seite verläuft ganz ähnlich wie bei einer normalen Anfrage: Es werden die gleichen Vorlagen geladen usw. Entscheidend ist jedoch, dass die Teile, die nicht in die Ausgabe gelangen sollen, weggelassen werden; die anderen Teile sollen mit einem Bezeichner verknüpft und in einem für einen JavaScript-Handler verständlichen Format an den Benutzer gesendet werden.
Syntax
Wenn ein Steuerelement oder ein Snippet in der Vorlage vorhanden ist, müssen wir es mit dem
{snippet} ... {/snippet}
pair-Tag einpacken – er sorgt dafür, dass das gerenderte Snippet “ausgeschnitten”
und an den Browser gesendet wird. Außerdem wird es in ein Hilfs-Tag eingebettet <div>
Tag (es ist möglich,
einen anderen zu verwenden). Im folgenden Beispiel wird ein Snippet mit dem Namen header
definiert. Es kann ebenso
gut die Vorlage einer Komponente darstellen:
{snippet header}
<h1>Hello ... </h1>
{/snippet}
Wenn Sie ein Snippet mit einem anderen Element erstellen möchten als <div>
erstellen oder dem Element
benutzerdefinierte Attribute hinzufügen möchten, können Sie die folgende Definition verwenden:
<article n:snippet="header" class="foo bar">
<h1>Hello ... </h1>
</article>
Dynamische Schnipsel
In Nette können Sie auch Snippets mit einem dynamischen Namen definieren, der auf einem Laufzeitparameter basiert. Dies eignet sich besonders für verschiedene Listen, bei denen nur eine Zeile geändert werden muss, aber nicht die ganze Liste mit übertragen werden soll. Ein Beispiel hierfür wäre:
<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>
Es gibt ein statisches Snippet namens itemsContainer
, das mehrere dynamische Snippets enthält:
item-0
, item-1
und so weiter.
Ein dynamisches Snippet kann nicht direkt neu gezeichnet werden (das erneute Zeichnen von item-1
hat keine
Auswirkung), Sie müssen das übergeordnete Snippet (in diesem Beispiel itemsContainer
) neu zeichnen. Dies bewirkt,
dass der Code des übergeordneten Snippets ausgeführt wird, aber nur seine Unter-Snippets an den Browser gesendet werden. Wenn
Sie nur einen der Teil-Snippets senden wollen, müssen Sie die Eingabe für das übergeordnete Snippet so ändern, dass die
anderen Teil-Snippets nicht erzeugt werden.
Im obigen Beispiel müssen Sie sicherstellen, dass bei einer AJAX-Anfrage nur ein Element zum Array $list
hinzugefügt wird, so dass die Schleife foreach
nur ein dynamisches Snippet ausgibt.
class HomePresenter extends Nette\Application\UI\Presenter
{
/**
* This method returns data for the list.
* Usually this would just request the data from a model.
* For the purpose of this example, the data is hard-coded.
*/
private function getTheWholeList(): array
{
return [
'First',
'Second',
'Third',
];
}
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');
}
}
Snippets in einer eingebundenen Vorlage
Es kann vorkommen, dass sich das Snippet in einer Vorlage befindet, die von einer anderen Vorlage eingebunden wird. In diesem
Fall müssen wir den Einbindungscode in der zweiten Vorlage mit dem snippetArea
-Makro einschließen, dann zeichnen
wir sowohl den Snippet-Bereich als auch das eigentliche Snippet neu.
Das Makro snippetArea
sorgt dafür, dass der darin enthaltene Code ausgeführt wird, aber nur das eigentliche
Snippet in der eingebundenen Vorlage an den Browser gesendet wird.
{* parent.latte *}
{snippetArea wrapper}
{include 'child.latte'}
{/snippetArea}
{* child.latte *}
{snippet item}
...
{/snippet}
$this->redrawControl('wrapper');
$this->redrawControl('item');
Sie können es auch mit dynamischen Snippets kombinieren.
Hinzufügen und Löschen
Wenn Sie ein neues Element in die Liste einfügen und itemsContainer
ungültig machen, liefert die AJAX-Anfrage
Snippets, die das neue Element enthalten, aber der Javascript-Handler kann es nicht darstellen. Das liegt daran, dass es kein
HTML-Element mit der neu erstellten ID gibt.
In diesem Fall besteht die einfachste Möglichkeit darin, die gesamte Liste in ein weiteres Snippet zu verpacken und alles ungültig zu machen:
{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');
}
Das Gleiche gilt für das Löschen eines Eintrags. Es wäre möglich, ein leeres Snippet zu senden, aber in der Regel können Listen paginiert werden, und es wäre kompliziert, das Löschen eines Elements und das Laden eines anderen (das sich auf einer anderen Seite der paginierten Liste befand) zu implementieren.
Parameter an die Komponente senden
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.
$.getJSON(
{link changeCountBasket!},
{
{$control->getParameterId('id')}: id,
{$control->getParameterId('count')}: count
}
});
Und behandeln Sie die Methode mit den entsprechenden Parametern in der Komponente.
public function handleChangeCountBasket(int $id, int $count): void
{
}