AJAX & Snippets

À l'ère des applications web modernes, où les fonctionnalités s'étendent souvent entre le serveur et le navigateur, AJAX est un élément de connexion essentiel. Quelles sont les possibilités offertes par le Nette Framework dans ce domaine ?

  • l'envoi de parties du modèle, appelées "snippets
  • le passage de variables entre PHP et JavaScript
  • outils de débogage des requêtes AJAX

Demande AJAX

Une requête AJAX ne diffère pas fondamentalement d'une requête HTTP classique. Un présentateur est appelé avec des paramètres spécifiques. C'est au présentateur de décider comment répondre à la requête – il peut renvoyer des données au format JSON, envoyer une partie du code HTML, un document XML, etc.

Du côté du navigateur, nous lançons une 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
});

Du côté du serveur, une requête AJAX est reconnue par la méthode $httpRequest->isAjax() du service encapsulant la requête HTTP. Elle utilise l'en-tête HTTP X-Requested-With, qu'il est donc essentiel d'envoyer. Dans le présentateur, vous pouvez utiliser la méthode $this->isAjax().

Si vous souhaitez envoyer des données au format JSON, utilisez la méthode sendJson() méthode. La méthode met également fin à l'activité du présentateur.

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

Si vous envisagez de répondre avec un modèle spécial conçu pour AJAX, vous pouvez procéder comme suit :

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

Bribes

Les snippets sont l'outil le plus puissant offert par Nette pour connecter le serveur au client. Grâce à eux, vous pouvez transformer une application ordinaire en une application AJAX avec un minimum d'effort et quelques lignes de code. L'exemple Fifteen montre comment tout cela fonctionne, et son code peut être trouvé sur GitHub.

Les snippets, ou clippings, vous permettent de ne mettre à jour que certaines parties de la page, au lieu de recharger toute la page. C'est plus rapide et plus efficace, et l'expérience utilisateur est 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 ? Lorsque la page est chargée pour la première fois (requête non-AJAX), la page entière, y compris tous les snippets, est chargée. Lorsque l'utilisateur interagit avec la page (par exemple, lorsqu'il clique sur un bouton, soumet un formulaire, etc.), au lieu de charger la page entière, une requête AJAX est effectuée. Le code du présentateur exécute l'action et décide quels extraits doivent être mis à jour. Nette rend ces extraits et les envoie sous la forme d'un tableau JSON. Le code de traitement du navigateur réintègre alors les extraits reçus dans la page. Par conséquent, seul le code des extraits modifiés est transféré, ce qui permet d'économiser de la bande passante et d'accélérer le chargement par rapport au transfert de l'ensemble du contenu de la page.

Naja

La bibliothèque Naja est utilisée pour gérer les snippets du côté du navigateur. Installez-la en tant que paquetage node.js (pour une utilisation avec des applications telles que Webpack, Rollup, Vite, Parcel, et d'autres) :

npm install naja

… ou insérez-la directement dans le modèle de page :

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

Vous devez d'abord initialiser la bibliothèque :

naja.initialize();

Pour faire d'un lien ordinaire (signal) ou d'une soumission de formulaire 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>

or

<form n:name="form">
    <input n:name="submit" class="ajax">
</form>

Redessiner des extraits

Chaque objet de la classe Control (y compris le Presenter lui-même) garde une trace des changements survenus qui nécessitent son redécoupage. La méthode redrawControl() est utilisée à cette fin.

public function handleLogin(string $user): void
{
	// après la connexion, il est nécessaire de redessiner la partie concernée
	$this->redrawControl();
	//...
}

Nette permet également un contrôle plus fin de ce qui doit être redessiné. La méthode susmentionnée peut prendre le nom de l'extrait comme argument. Ainsi, il est possible d'invalider (c'est-à-dire de forcer un nouveau dessin) au niveau de la partie du modèle. Si l'ensemble du composant est invalidé, chaque extrait est également redessiné :

// invalide l'extrait "header" (en-tête)
$this->redrawControl('header');

Bribes dans Latte

L'utilisation des snippets dans Latte est extrêmement simple. Pour définir une partie du modèle comme un extrait, il suffit de l'entourer des balises {snippet} et {/snippet}:

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

Le snippet crée un élément <div> dans la page HTML avec un id spécialement généré. Lorsqu'un extrait est redessiné, le contenu de cet élément est mis à jour. Par conséquent, lors du rendu initial de la page, tous les snippets doivent également être rendus, même s'ils sont initialement vides.

Vous pouvez également créer un extrait avec un élément autre que <div> à l'aide d'un attribut n: :

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

Zones d'échantillonnage

Les noms des snippets peuvent également être des expressions :

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

De cette manière, nous obtiendrons plusieurs snippets comme item-0, item-1, etc. Si nous devions invalider directement un extrait dynamique (par exemple, item-1), rien ne serait redessiné. La raison en est que les snippets fonctionnent comme de véritables extraits et qu'ils sont les seuls à être rendus directement. Cependant, dans le modèle, il n'y a pas techniquement d'extrait nommé item-1. Il n'apparaît que lors de l'exécution du code environnant de l'extrait, dans ce cas, la boucle foreach. Par conséquent, nous marquerons la partie du modèle qui doit être exécutée avec la balise {snippetArea}:

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

Et nous redessinerons à la fois l'extrait individuel et l'ensemble de la zone globale :

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

Il est également essentiel de s'assurer que le tableau $items ne contient que les éléments qui doivent être redessinés.

Lors de l'insertion d'un autre modèle dans le modèle principal à l'aide de la balise {include}, qui contient des extraits, il est nécessaire d'envelopper à nouveau le modèle inclus dans une balise snippetArea et d'invalider à la fois l'extrait et la zone :

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

Les snippets dans les composants

Vous pouvez créer des snippets dans les composants, et Nette les redessinera automatiquement. Cependant, il y a une limitation spécifique : pour redessiner les snippets, il faut appeler la méthode render() sans aucun paramètre. Par conséquent, passer des paramètres dans le modèle ne fonctionnera pas :

OK
{control productGrid}

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

Envoi de données utilisateur

En plus des snippets, vous pouvez envoyer des données supplémentaires au client. Il suffit de les écrire dans l'objet payload:

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

Paramètres d'envoi

Lorsque nous envoyons des paramètres au composant via une requête AJAX, qu'il s'agisse de paramètres de signal ou de paramètres persistants, nous devons fournir leur nom global, qui contient également le nom du composant. Le nom complet du paramètre renvoie la méthode getParameterId().

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

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

Une méthode handle avec les paramètres correspondants dans le composant :

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