Donner au site web la sensation d'une SPA
Le site web est rapide, mais chaque clic recharge encore une page HTML complète. La réponse traditionnelle consiste à construire une Single-Page Application (SPA) : une application JavaScript, écrite dans une autre stack, qui dialogue avec l'API. Cela signifie une seconde application à développer, sécuriser, déployer et garder synchronisée avec la principale.
Symfony a une réponse différente. Symfony UX est une initiative qui apporte l'expérience d'une SPA aux applications rendues côté serveur : continuez à écrire des templates Twig et des contrôleurs Symfony, et saupoudrez du JavaScript uniquement là où il apporte de la valeur.
Découvrir Symfony UX
La bonne nouvelle ? Nous utilisons Symfony UX depuis la toute première étape de ce livre. Le squelette webapp embarque deux de ses paquets : symfony/stimulus-bundle et symfony/ux-turbo. Jetez un coup d'œil à importmap.php :
1 2 3 4 5 6
return [
'app' => ['path' => './assets/app.js', 'entrypoint' => true],
'@hotwired/stimulus' => ['version' => '3.2.2'],
'@symfony/stimulus-bundle' => ['path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js'],
'@hotwired/turbo' => ['version' => '8.0.23'],
];
Stimulus est un petit framework JavaScript qui connecte des contrôleurs JavaScript à des éléments HTML via des attributs data-. Le fichier assets/stimulus_bootstrap.js démarre l'application Stimulus et enregistre automatiquement tout contrôleur stocké dans le répertoire assets/controllers/.
Turbo s'appuie sur cette fondation pour rendre la navigation instantanée.
Naviguer instantanément avec Turbo
Turbo Drive travaille silencieusement depuis le début : il intercepte chaque clic sur un lien et chaque soumission de formulaire, récupère la page en arrière-plan et remplace le <body> sans rechargement complet de la page. Le navigateur garde les mêmes JavaScript et CSS actifs, donc la navigation semble immédiate.
Ouvrez les outils de développement du navigateur, passez sur l'onglet "Network" et cliquez à travers le site : les changements de page sont des requêtes fetch, pas des chargements complets de document.
Turbo ne demande qu'une chose en retour : une sémantique HTTP correcte pour les formulaires. Une soumission réussie doit rediriger (la nôtre le fait, grâce à redirectToRoute()), et une soumission échouée doit utiliser un code de statut 4xx. Symfony gère ce dernier cas automatiquement : quand un formulaire soumis est invalide, render() répond avec un code de statut 422 Unprocessable Entity.
Mettre à jour une partie de page avec les Turbo Frames
Turbo Drive évite les rechargements complets de page. Les Turbo Frames vont un cran plus loin : elles permettent à un fragment de la page de naviguer indépendamment du reste.
La liste des commentaires est une candidate parfaite. Lorsqu'on parcourt les pages de commentaires via les liens "Previous" et "Next", seule la liste devrait changer ; régénérer l'en-tête, le formulaire et le pied de page est du travail inutile. Enveloppez les commentaires dans une frame :
1 2 3 4 5 6 7 8 9 10 11 12 13 14
--- i/templates/conference/show.html.twig
+++ w/templates/conference/show.html.twig
@@ -17,3 +17,4 @@
<div class="row">
<div class="col-12 col-lg-8">
+ <turbo-frame id="comments" data-turbo-action="advance">
{% if comments|length > 0 %}
@@ -56,5 +57,6 @@
No comments have been posted yet for this conference.
</div>
{% endif %}
+ </turbo-frame>
</div>
<div class="col-12 col-lg-4">
Les liens à l'intérieur d'un élément <turbo-frame> ne mettent à jour que cette frame : en cliquant sur "Next", Turbo récupère la page en arrière-plan et remplace la frame correspondante, en laissant le reste de la page intact.
L'attribut data-turbo-action="advance" promeut la navigation de la frame en visite complète : l'URL dans la barre d'adresse est mise à jour (y compris le paramètre de requête offset), de sorte que les commentaires paginés restent partageables et que le bouton retour du navigateur fonctionne comme attendu.
Prévisualiser la photo du commentaire avec Stimulus
Il est temps d'écrire nos premières lignes de JavaScript de tout le livre. Les participants qui soumettent une photo ne peuvent pas la voir avant de poster le commentaire. Affichons un aperçu quand ils sélectionnent un fichier.
Créez un contrôleur Stimulus (le nom du fichier détermine le nom du contrôleur, photo-preview) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
show() {
const [photo] = this.element.files;
if (!photo) {
this.preview?.remove();
this.preview = null;
return;
}
if (!this.preview) {
this.preview = document.createElement('img');
this.preview.className = 'img-thumbnail mt-2';
this.preview.style.maxHeight = '150px';
this.element.insertAdjacentElement('afterend', this.preview);
}
this.preview.src = URL.createObjectURL(photo);
}
}
C'est tout le JavaScript dont nous avons besoin. Stimulus découvre et enregistre le contrôleur automatiquement. Connectez-le au champ photo du formulaire de commentaire via des attributs data- :
1 2 3 4 5 6 7 8 9 10 11 12 13
--- i/src/Form/CommentType.php
+++ w/src/Form/CommentType.php
@@ -26,6 +26,10 @@ class CommentType extends AbstractType
->add('photo', FileType::class, [
'required' => false,
'mapped' => false,
+ 'attr' => [
+ 'data-controller' => 'photo-preview',
+ 'data-action' => 'change->photo-preview#show',
+ ],
'constraints' => [
new Image(maxSize: '1024k')
],
L'attribut data-controller lie le contrôleur au champ de fichier, et data-action appelle sa méthode show() chaque fois que la valeur du champ change. Sélectionnez une photo sur une page de conférence et profitez de l'aperçu.
Les contrôleurs Stimulus fonctionnent main dans la main avec Turbo : comme Drive et les Frames mettent à jour la page sans la recharger, les contrôleurs sont automatiquement connectés et déconnectés à mesure que les éléments entrent et sortent du DOM.
Et les applications mobiles ?
Nous avons obtenu l'expérience d'une SPA sans construire une seconde application : pas de stack JavaScript séparée, pas de second serveur web, pas de configuration CORS, rien de nouveau à déployer.
Et si vous avez un jour besoin d'une application mobile native, l'API créée à l'étape précédente est le bon point d'entrée : elle est publique, documentée et agnostique du framework. Le site web et l'application mobile peuvent évoluer indépendamment tout en partageant le même backend.