Agrégateur de contenus
Développer une Single Page Application avec Turbo - Partie 2
Rédigé par Benjamin Dreux le 23 janv. 2024
Dans le premier article de cette série, nous avons discuté du contexte dans lequel s'insèrent des outils tels que Turbo et htmx, ainsi que de leur fonctionnement de base.
Il est maintenant temps de passer à l'examen du code.
Installation de Turbo
Turbo se trouve à l’adresse suivante: https://github.com/hotwired/turbo/releases
Si vous souhaitez utiliser la version compilée: https://cdn.skypack.dev/@hotwired/turbo
<script type="module">
import hotwiredTurbo from '<https://cdn.skypack.dev/@hotwired/turbo';>
</script>
Pour les autres moyens d’installation, consultez: https://turbo.hotwired.dev/handbook/installing
Impact de Turbo dans votre app
Avec ce simple script, voici ce qui va se passer automatiquement : Les clics sur des liens ou les envois de formulaire seront interceptés. Au lieu de suivre leur fonctionnement normal, le même envoi sera effectué (POST, GET, ou autre), mais à travers l’API Fetch (https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API ).
-
Lorsque la nouvelle page sera générée par le backend, Turbo la divisera en deux parties : le head et le body.
-
Turbo combinera le head de notre page actuelle avec celui de la page nouvellement arrivée. En ce qui concerne le contenu (le body), il remplacera le body de la page actuelle.
-
Si le temps de traitement d'une requête est long, une barre de chargement sera affichée en haut de l'écran et le bouton/lien que vous venez de cliquer deviendra désactivé (pour éviter un double envoi).
-
Le backend n'a pas besoin d'être modifié pour cela.
Limitations
Voici quelques limitations / ajustements que vous allez devoir prendre en compte pour l’écriture de votre application.
Désactivation de turbo pour certains liens
Si vous souhaitez créer un lien permettant le téléchargement d'un PDF ou qui redirige en dehors de votre application, il ne doit pas recevoir le traitement habituel réservé aux autres liens de votre application.
Pour désactiver le lien, il faut appliqué le changement suivant:
<a href="..." data-turbo="false">Télécharger ce document</a>
Cet attribut demande explicitement à Turbo de ne pas effectuer son traitement habituel sur ce lien en particulier.
Chargement de javascript
Les changements apportés par Turbo au traitement du HTML font en sorte qu'au lieu de créer un nouveau contexte d'exécution à chaque nouvelle page, on utilise le même contexte pour l'ensemble de la session, comme on le ferait pour une application à page unique (SPA).
Il est important de rappeler que les scripts, ainsi que tout ce qui se trouve dans la balise head, sont fusionnés entre ce qui était présent sur la page précédente et ce qui est reçu pour la page actuelle. Ainsi, si un script a déjà été chargé par une page précédente, il ne sera pas ajouté de nouveau dans le contexte de la session. Par conséquent, le code qui doit s'exécuter au chargement d'un script ne sera pas exécuté deux fois.
Il existe au moins deux solutions pour résoudre ce problème :
1- Ajouter des balises script directement dans les pages.
La première technique consiste a importer du code qui ne déclenche pas de calcul dans le head, de manière classique. Combiné avec du code JS dans le body qui s’exécutera au moment du chargement de la page.
2-Déclencher du code JavaScript au changement de pages.
La seconde est plus difficile à mettre en place. Il s’agit d’écouter des évènements produits par turbo lorsqu’on change de page. Heureusement il y’a une librairie pour nous aider avec ceci, Stimulus https://stimulus.hotwired.dev/.
Nous reviendrons dans un autre article sur cette librairie.
Toutefois, la chose importante à retenir est que, dans la plupart des cas, nous n'aurons plus besoin d'écrire beaucoup de JavaScript. Nous avons implémenté une application de gestion de feuille de temps, avec seulement 1 000 lignes de JavaScript, tout en offrant une haute fidélité aux utilisateurs qui, sans ouvrir les devtools, ne sont généralement pas capables de deviner s’il s’agit d’une application à page unique (SPA) ou à pages multiples( Multi-page app).
Décomposition avec turbo-frame
Ce que nous avons présenté jusqu'ici existait déjà dans Turbolinks, l’ancêtre de Turbo. Depuis, 37signals a introduit une autre idée dans le mélange de fonctionnalités. Lorsque nous avons commencé à présenter mes premières applications web, plusieurs tutoriels tels que celui-ci existaient : https://www.w3schools.com/js/js_ajax_intro.asp . GitHub a également utilisé cette technique à travers AJAX : https://github.com/defunkt/jquery-pjax .
Ces tutoriels expliquent comment, en JavaScript avec l’aide d’une requête jQuery, on peut rendre une page dynamique. Dans cet exemple, on va chercher un fichier texte sur le serveur et on affiche son contenu dans n éléments donnés. Cela se fait de manière impérative et très verbeuse.
Ce que Turbo apporte, c’est une manière déclarative assez élégante de faire la même chose. La librairie introduit un nouvel élément HTML (un custom élément https://developer.mozilla.org/en-US/docs/Web/API/Web_Components/Using_custom_elements ). Cet élément est le suivant :
htmlCopy code
<turbo-frame> … </turbo-frame>
Cet élément n’a pas d’impact visuel dans la page web, contrairement à une balise <table>
, <hr>
. Mais, comme un <form>
, il permet des fonctionnalités supplémentaires. Quand un lien est à l’intérieur d’un turbo-frame, au lieu de changer le contenu de toute la page lorsque l’on clique sur ce lien, c’est seulement le contenu de ce turbo-frame qui est mis à jour. De la même manière, si un formulaire est envoyé depuis l’intérieur d’un turbo-frame, c’est le contenu du turbo-frame qui est mis à jour, au lieu de la page complète. En quelque sorte, un turbo-frame est une frontière de mise à jour.
Afin que Turbo puisse déterminer quelle partie de la nouvelle page doit être placée dans le turbo-frame de l’ancienne, le turbo-frame de départ doit être doté d'un attribut id. Ce même id doit également être présent sur un autre turbo-frame dans la nouvelle page. Cela implique que la nouvelle page peut être soit une page complète, soit une portion de page.
Utilisation de turbo-frame
Imaginons une liste d’épicerie où l’on souhaiterait pouvoir effectuer des éditions élément par élément. Pour cela, nous pourrions avoir un morceau de HTML qui ressemblerait à ceci:
<ul>
<li>
<turbo-frame id="list_21_item_44">
<a href="edit">Concombre</a>
</turbo-frame>
</li>
<li>
<turbo-frame id="list_21_item_82">
<a href="edit">Choux</a>
</turbo-frame>
</li>
<li>
<turbo-frame id="list_21_item_100">
<a href="edit">Radis</a>
</turbo-frame>
</li>
<ul>
Lorsque l’utilisateur clique sur l'un des liens définis, seul le contenu de son turbo-frame parent sera modifié. Dans cet exemple, imaginons que le dernier élément de la liste soit sélectionné. Nous pourrions alors nous retrouver sur la page suivante :
<ul>
<li>
<turbo-frame id="list_21_item_44">
<a href="edit">Concombre</a>
</turbo-frame>
</li>
<li>
<turbo-frame id="list_21_item_82">
<a href="edit">Choux</a>
</turbo-frame>
</li>
<li>
<turbo-frame id="list_21_item_100">
<form method="POST" action="/list/21/item/100/save">
<input type=text name="name" value="Radis"/>
<button>Save</button>
</form>
</turbo-frame>
</li>
<ul>
Ici, on peut observer qu'un formulaire a été injecté à la place du lien qui occupait cet emplacement précédemment. Bien que la réponse de l'application web puisse être une page complète, Turbo ne prendra en compte que le turbo-frame avec l’identifiant 'list_21_item_100'. Ainsi, la réponse peut être aussi simple que ceci :
<turbo-frame id="list_21_item_100">
<form method="POST" action="/list/21/item/100/save">
<input type=text name="name" value="Radis"/>
<button>Save</button>
</form>
</turbo-frame>
L’avantage d'avoir une page de retour dépouillée comme celle-ci est qu’elle sera extrêmement rapide à générer. Seule l'information rigoureusement nécessaire doit être récupérée de la base de données, et le HTML produit est réduit à sa plus simple expression. Comme le template est généré dans le backend, nous conservons toute la souplesse du moteur de template auquel nous sommes habitués. Par exemple, nous pouvons inclure des éléments.
Page partiel
Les turbo-frame permettent également de diviser le chargement de la page en plusieurs morceaux. Imaginons une application dans laquelle des compteurs doivent être affichés en plus d’une liste.
Comme on peut le voir dans ce mockup, la partie la plus importante de ce design n'est pas le compteur, mais plutôt la liste qui est affichée. Par contre, dans notre scénario hypothétique, obtenir les chiffres à afficher dans les compteurs pourrait être un processus long.
Dans ce contexte, une solution pourrait être de décaler dans le temps. Ainsi, le premier chargement permettrait à la majorité des utilisateurs de voir les informations qui les importent. Et ceux qui souhaitent obtenir cette information ne l'auront que quelques centaines de millisecondes plus tard.
Voici à quoi pourrait ressembler notre HTML :
<turbo-frame id="compteur-users" src="/compteurs/users">
<div class="comtpeur">
</div>
</turbo-frame>
Ici, ce que l’on peut observer, c’est un turbo-frame avec un attribut src que nous n’avons pas encore utilisé. Cet attribut signifie que lorsque le turbo-frame est interprété, une requête est lancée vers l’URL pointée dans l’attribut src. La réponse obtenue à cette requête sera chargée selon la règle habituelle (turbo-frame avec le même identifiant).
Mais alors, pourquoi mettre un contenu dans notre turbo-frame si c’est pour le faire remplacer rapidement ? Rappelons-nous qu’obtenir ce nombre est relativement coûteux en temps, cela l'était quand on faisait tout dans une seule page, et cela l'est encore dans une page distincte. Pour compenser cela, deux alternatives sont possibles :
-
Mettre en place un loader qui tourne pendant que l’on attend d’avoir la page partielle.
-
Afficher un contenu qui sera le plus proche possible de notre page partielle, de sorte que lorsque le contenu arrive, la transition soit la plus douce possible.
Un bénéfice intéressant dans le backend qui gère notre page est la réduction du nombre de tâches nécessaires pour compléter le calcul d’une page. Cela signifie qu’il est plus facile de suivre les opérations à faire pour cette page. Et comme pour le formulaire dans l’exemple précédent, on obtient des endpoints qui sont précis et clairs dans leur intention.
Page Partiel paresseuse
Avec ce que l’on vient de voir, il est possible d’avoir plusieurs morceaux d’écrans qui s’affichent progressivement au fur et à mesure que l’on obtient les données. Cependant, il se peut que le morceau d’écran que l’on obtient ne soit même pas encore nécessaire.
Imaginons que l’on veut afficher une grande liste ; la logique classique est de mettre en place un système de pagination. Toutes les bases de données offrent des fonctionnalités pour supporter ce genre de fonctionnalité. Un de ces problèmes de design est que l’interface est encombrée avec un concept qui n’a de réalité que pour aider le backend. De plus, si vous souhaitez faire une recherche parmi les 10 pages que vous venez de voir, il va falloir répéter cette recherche dix fois.
Pour mettre en place un chargement paresseux, rien de plus simple :
<turbo-frame id="mon-id" src="/messages/123" loading="lazy">
...
</turbo-frame>
Avec l’attribut loading="lazy"
, le turbo-frame ne lancera pas de requête tant qu'il ne sera pas visible dans la fenêtre du navigateur.
Maintenant que nous avons cette nouvelle fonctionnalité, il devient facile de construire une page avec un défilement infini (infinit-scroll). L’idée ici est d’afficher une liste d’éléments et, en bas de la liste, d'ajouter un turbo-frame qui permettra d’afficher plus d’éléments.
<div class="list>
<div class="item>...</div>
<div class="item>...</div>
<div class="item>...</div>
<div class="item>...</div>
<turbo-frame src="/list/more?page=12&sort=id&size=100" loading="lazy">
Chargement en cours
</turbo-frame>
</div>
Ce qui est crucial dans cette technique c’est que la liste qui est produite inclut un turbo-frame en bas de liste.
L’url de l’attribut src, nous permettra d’obtenir la suite de la liste, comme avec une pagination.
Pour que le tout fonctionne il faut évidement que l’url pointée par le turbo-frame nous retour d’autres éléments et un nouveau turbo-frame qui pourra lui aussi déclencher la demande d’une autre sous-liste.
Pour terminer
En résumé, nous avons vu comment intégrer Turbo dans n'importe quelle application multipage et comment les applications Turbo peuvent améliorer la perception de la rapidité de traitement d’une application. Nous avons également, à travers quelques exemples, montré comment tirer profit des capacités offertes par les turbo-frames.
Avec ces quelques techniques, dans ma pratique courante, j'ai rarement besoin de plus. Ce qui m'arrive fréquemment en plus de cela, c'est d’avoir besoin d’initialiser des composants JavaScript.