AJAX & Snippets
À l'ère des applications web modernes, où la fonctionnalité est souvent répartie entre le serveur et le navigateur, AJAX est un élément de liaison essentiel. Quelles possibilités Nette Framework nous offre-t-il dans ce domaine ?
- envoi de parties de template, appelées snippets
- transmission de variables entre PHP et JavaScript
- outils pour le débogage des requêtes AJAX
Requête AJAX
Une requête AJAX ne diffère fondamentalement pas d'une requête HTTP classique. Un presenter est appelé avec certains paramètres. Et c'est au presenter de décider comment réagir à la requête – il peut retourner des données au format JSON, envoyer une partie du code HTML, un document XML, etc.
Côté navigateur, nous initialisons la requête AJAX à l'aide de la fonction fetch()
:
fetch(url, {
headers: {'X-Requested-With': 'XMLHttpRequest'},
})
.then(response => response.json())
.then(payload => {
// traitement de la réponse
});
Côté serveur, nous reconnaissons une requête AJAX avec la méthode $httpRequest->isAjax()
du service encapsulant la requête HTTP. Pour la détection, il utilise l'en-tête HTTP
X-Requested-With
, il est donc important de l'envoyer. Au sein du presenter, vous pouvez utiliser la méthode
$this->isAjax()
.
Si vous souhaitez envoyer des données au format JSON, utilisez la méthode sendJson()
. La méthode termine
également l'activité du presenter.
public function actionExport(): void
{
$this->sendJson($this->model->getData);
}
Si vous prévoyez de répondre en utilisant un template spécial conçu pour AJAX, vous pouvez le faire comme suit :
public function handleClick($param): void
{
if ($this->isAjax()) {
$this->template->setFile('path/to/ajax.latte');
}
// ...
}
Snippets
Le moyen le plus puissant offert par Nette pour connecter le serveur et le client sont les snippets. Grâce à eux, vous pouvez transformer une application ordinaire en une application AJAX avec un minimum d'effort et quelques lignes de code. Le fonctionnement est démontré par l'exemple Fifteen, dont le code se trouve sur GitHub.
Les snippets, ou extraits, permettent de mettre à jour uniquement des parties de la page, au lieu de recharger la page entière. C'est non seulement plus rapide et plus efficace, mais cela offre également une expérience utilisateur plus confortable. Les snippets peuvent vous rappeler Hotwire pour Ruby on Rails ou Symfony UX Turbo. Il est intéressant de noter que Nette a introduit les snippets 14 ans plus tôt.
Comment fonctionnent les snippets ? Lors du premier chargement de la page (requête non-AJAX), la page entière est chargée, y compris tous les snippets. Lorsque l'utilisateur interagit avec la page (par exemple, clique sur un bouton, soumet un formulaire, etc.), une requête AJAX est déclenchée au lieu de charger la page entière. Le code dans le presenter exécute l'action et décide quels snippets doivent être mis à jour. Nette rend ces snippets et les envoie sous forme de tableau au format JSON. Le code de gestion dans le navigateur réinsère les snippets reçus dans la page. Ainsi, seul le code des snippets modifiés est transféré, ce qui économise de la bande passante et accélère le chargement par rapport au transfert du contenu de la page entière.
Naja
Pour gérer les snippets côté navigateur, la bibliothèque Naja est utilisée. Installez-la en tant que paquet node.js (pour une utilisation avec des applications comme Webpack, Rollup, Vite, Parcel, et autres) :
npm install naja
…ou insérez-la directement dans le template de la page :
<script src="https://unpkg.com/naja@2/dist/Naja.min.js"></script>
Tout d'abord, la bibliothèque doit être initialisée :
naja.initialize();
Pour transformer un lien ordinaire (signal) ou une soumission de formulaire en une requête AJAX, il suffit de marquer le lien,
le formulaire ou le bouton correspondant avec la classe ajax
:
<a n:href="go!" class="ajax">Go</a>
<form n:name="form" class="ajax">
<input n:name="submit">
</form>
ou
<form n:name="form">
<input n:name="submit" class="ajax">
</form>
Redessiner les Snippets
Chaque objet de la classe Control (y compris le Presenter
lui-même) enregistre si des changements nécessitant son redessin se sont produits. La méthode redrawControl()
est
utilisée pour cela :
public function handleLogin(string $user): void
{
// après la connexion, la partie pertinente doit être redessinée
$this->redrawControl();
// ...
}
Nette permet un contrôle encore plus fin de ce qui doit être redessiné. En effet, la méthode mentionnée peut accepter le nom du snippet comme argument. Il est donc possible d'invalider (c'est-à-dire : forcer le redessin) au niveau des parties du template. Si le composant entier est invalidé, chacun de ses snippets sera également redessiné :
// invalide le snippet 'header'
$this->redrawControl('header');
Snippets dans Latte
L'utilisation des snippets dans Latte est extrêmement facile. Pour définir une partie du template comme snippet,
enveloppez-la simplement avec les balises {snippet}
et {/snippet}
:
{snippet header}
<h1>Bonjour ... </h1>
{/snippet}
Le snippet crée un élément <div>
dans la page HTML avec un id
spécial généré. Lors du
redessin du snippet, le contenu de cet élément est mis à jour. Il est donc nécessaire que lors du rendu initial de la page,
tous les snippets soient également rendus, même s'ils peuvent être vides au début.
Vous pouvez également créer un snippet avec un élément autre que <div>
en utilisant un n:attribut :
<article n:snippet="header" class="foo bar">
<h1>Bonjour ... </h1>
</article>
Zones de Snippets
Les noms des snippets peuvent aussi être des expressions :
{foreach $items as $id => $item}
<li n:snippet="item-{$id}">{$item}</li>
{/foreach}
Cela crée plusieurs snippets item-0
, item-1
, etc. Si nous invalidions directement un snippet
dynamique (par exemple item-1
), rien ne serait redessiné. La raison est que les snippets fonctionnent vraiment comme
des extraits et ne sont rendus qu'eux-mêmes directement. Cependant, il n'y a en fait aucun snippet nommé item-1
dans le template. Il n'est créé que lors de l'exécution du code autour du snippet, c'est-à-dire la boucle foreach. Nous
marquons donc la partie du template qui doit être exécutée à l'aide de la balise {snippetArea}
:
<ul n:snippetArea="itemsContainer">
{foreach $items as $id => $item}
<li n:snippet="item-{$id}">{$item}</li>
{/foreach}
</ul>
Et nous laissons redessiner à la fois le snippet lui-même et toute la zone parente :
$this->redrawControl('itemsContainer');
$this->redrawControl('item-1');
En même temps, il est conseillé de s'assurer que le tableau $items
ne contient que les éléments qui doivent
être redessinés.
Si nous insérons un autre template contenant des snippets dans le template principal à l'aide de la balise
{include}
, il est nécessaire d'inclure à nouveau l'insertion du template dans une snippetArea
et de
l'invalider avec le snippet :
{snippetArea include}
{include 'included.latte'}
{/snippetArea}
{* included.latte *}
{snippet item}
...
{/snippet}
$this->redrawControl('include');
$this->redrawControl('item');
Snippets dans les Composants
Vous pouvez également créer des snippets dans les composants et
Nette les redessinera automatiquement. Mais il y a une certaine limitation : pour redessiner les snippets, il appelle la méthode
render()
sans paramètres. Par conséquent, la transmission de paramètres dans le template ne fonctionnera
pas :
OK
{control productGrid}
ne fonctionnera pas :
{control productGrid $arg, $arg}
{control productGrid:paginator}
Envoi de Données Utilisateur
Avec les snippets, vous pouvez envoyer n'importe quelles autres données au client. Il suffit de les écrire dans l'objet
payload
:
public function actionDelete(int $id): void
{
// ...
if ($this->isAjax()) {
$this->payload->message = 'Succès';
}
}
Transmission de Paramètres
Si nous envoyons des paramètres à un composant via une requête AJAX, qu'il s'agisse de paramètres de signal ou de
paramètres persistants, nous devons spécifier leur nom global dans la requête, qui inclut également le nom du composant. Le
nom complet du paramètre est retourné par la méthode getParameterId()
.
let url = new URL({link //foo!});
url.searchParams.set({$control->getParameterId('bar')}, bar);
fetch(url, {
headers: {'X-Requested-With': 'XMLHttpRequest'},
})
Et la méthode handle avec les paramètres correspondants dans le composant :
public function handleFoo(int $bar): void
{
}