Htmx et Django pour les Single Page Applications

Temps de lecture ~7 minutes

Nous ne sommes pas très fans des gros frameworks Javascript chez NLP Cloud. NLP Cloud est une API qui se base sur spaCy et les transformers HuggingFace afin de proposer Named Entity Recognition (NER), analyse de sentiments, classification de texte, summarization, et bien plus.Notre backoffice est très simple. Les utilisateurs peuvent récupérer leur token API, uploader leurs modèles spaCy custom, upgrader leur plan, envoyer un message au support… Rien de très complexe donc nous n’avons pas ressenti le besoin d’utiliser Vue.js ou React.js. Nous avons décider d’opter pour une combinaison très sympa de htmx et Django.

Laissez-moi vous montrer comment ça fonctionne et vous exposer les avantages de cette solution.

Qu’est-ce que htmx et pourquoi l’utiliser ?

htmx est le successeur d’intercooler.js. Le concept derrière ces 2 projets est que vous pouvez effectuer toute sorte d’opération avancée comme de l’AJAX, des transitions CSS, des websockets, etc. uniquement avec du HTML (c’est à dire sans une seule ligne de Javascript). Et la lib est très légère (9Ko seulement).

Une autre chose très intéressante est que, lorsque vous effectuez des appels asynchrones vers votre backend, htmx n’attend pas du JSON mais des fragment de HTML. Donc en gros, contrairement à Vue.js ou React.js, votre frontend n’a pas besoin de gérer du JSON. Il suffit de remplacer les parties du DOM devant être modifiées par le fragment HTML retourné par le serveur. Cela vous permet de tirer parti au mieux de votre bon vieux framework backend (templates, sessions, authentification, etc.) au lieu de le transformer en un framework headless qui retourne uniquement du JSON. L’idée est qu’un fragment HTML étant à peine plus lourd qu’un JSON, la différence est négligeable dans une requête HTTP.

Ainsi, pour résumer, voici pourquoi htmx est intéressant lorsque vous souhaitez construire une Single Page Application (SPA) :

  • pas de Javascript à écrire,
  • les excellents frameworks backend comme Django, Ruby On Rails, Laravel… peuvent être utilisés à plein régime,
  • la lib est très légère (9Ko) comparée aux frameworks comme Vue ou React,
  • aucun pré-processing n’est nécessaire (Webpack, Babel, etc.) ce qui rend l’expérience de développement beaucoup plus sympa.

Installation

L’installation de htmx revient juste à charger le script dans votre <head> HTML:

<script src="https://unpkg.com/htmx.org@1.2.1"></script>

Je n’entrerai pas dans les détails de l’installation de Django ici puisque cet article se concentre sur htmx.

Charger du contenu en asynchrone

Le point le plus important, lorsque vous créez une SPA, c’est que vous vous attendez à ce que tous les chargements d’éléments se fassent en asynchrone. Par exemple, lorsque vous cliquez sur un menu afin d’aller vers une nouvelle page, vous ne voulez pas que l’intégralité de la page se recharge, mais uniquement le contenu qui a changé. Voici comment s’y prendre.

Disons que notre site est fait de 2 pages :

  • la page token qui montre à l’utilisateur son token API,
  • la page support qui en gros montre l’email du support à l’utilisateur.

On souhaite aussi afficher une barre de chargement pendant que la nouvelle page charge.

Frontend

Côté frontend, commençons par créer un menu à 2 entrées. Cliquer sur une entrée va afficher la barre de chargement, et changer le contenu de la page sans la recharger intégralement.

<progress id="content-loader" class="htmx-indicator" max="100"></progress>
<aside>
    <ul>
        <li><a hx-get="/token" hx-push-url="true"
                hx-target="#content" hx-swap="innerHTML" 
                hx-indicator="#content-loader">Token</a></li>
        <li><a hx-get="/support"
                hx-push-url="true" hx-target="#content" hx-swap="innerHTML"
                hx-indicator="#content-loader">Support</a></li>
    </ul>
</aside>
<div id="content">Hello and welcome to NLP Cloud!</div>

Dans l’exemple ci-dessus, la barre de chargement est l’élément <progress>. Il est caché par défaut grâce à la classe htmx-indicator. Lorsqu’un utilisateur clique sur une des deux entrées du menu, cela affiche la barre de chargement, grâce à hx-indicator="#content-loader".

Lorsqu’un utilisateur clique sur l’entrée token, cela déclenche un appel GET asynchrone vers l’url Django token grâce à hx-get="/token". Django retourne un fragment HTML que htmx place dans <div id="content"></div> grâce à hx-target="#content" hx-swap="innerHTML".

Il en est de même pour la page support.

Même si la page ne s’est pas rechargée, nous voulons malgré tout mettre à jour l’URL du navigateur afin d’aider l’utilisateur à se situer. C’est pour cette raison que nous utilisons hx-push-url="true".

Comme vous pouvez le voir, nous avons désormais une SPA qui utilise des fragments HTML au lieu de JSON, grâce à une simple lib de 9Ko, et seulement quelques directives à appliquer dans le HTML.

Backend

Bien entendu, la partie ci-dessus ne fonctionne pas sans le backend Django.

Voici votre urls.py:

from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('token', views.token, name='token'),
    path('support', views.support, name='support'),
]

Maintenant votre views.py:

def index(request):
    return render(request, 'backoffice/index.html')

def token(request):
    api_token = 'fake_token'

    return render(request, 'backoffice/token.html', {'token': api_token})

def support(request):
    return render(request, 'backoffice/support.html')

Et enfin, dans un répertoire templates/backoffice ajoutez les templates suivants.

index.html (c’est à dire le code que nous avons écrit ci-dessus mais cette fois avec des les tags de template {% url %}) :

<!DOCTYPE html>
<html>
    <head>
        <script src="https://unpkg.com/htmx.org@1.2.1"></script>
    </head>

    <body>
        <progress id="content-loader" class="htmx-indicator" max="100"></progress>
        <aside>
            <ul>
                <li><a hx-get="{% url 'home' %}"
                        hx-push-url="true" hx-target="#content" hx-swap="innerHTML"
                        hx-indicator="#content-loader">Home</a></li>
                <li><a hx-get="{% url 'token' %}" hx-push-url="true"
                        hx-target="#content" hx-swap="innerHTML" 
                        hx-indicator="#content-loader">Token</a></li>
            </ul>
        </aside>
        <div id="content">Hello and welcome to NLP Cloud!</div>
    <body>
</html>

token.html:

Here is your API token: {{ token }}

support.html:

For support questions, please contact support@nlpcloud.io

Comme vous pouvez le voir, tout cela est du pur code Django utilisant des routes et des templates, comme à l’accoutumée. Nul besoin d’une API et de Django Rest Framework ici.

Permettre le rafraîchissement manuel des pages

Le souci avec la solution ci-dessus c’est que, si un utilisateur recharge manuellement les pages token ou support, il obtiendra uniquement le fragment HTML au lieu de la page HTML toute entière.

La solution, côté Django, est de retourner 2 templates différents selon que la requête vient d’htmx ou non.

Voici comment faire.

Dans votre views.py, testez si le header HTTP_HX_REQUEST a été passé dans la requête. Si c’est le cas, cela signifie qu’il s’agit d’une requête de htmx et que dans ce cas vous pouvez afficher le fragment HTML seulement. Dans le cas contraire, vous devez afficher la page entière.

def index(request):
    return render(request, 'backoffice/index.html')

def token(request):
    if request.META.get("HTTP_HX_REQUEST") != 'true':
        return render(request, 'backoffice/token_full.html', {'token': api_token})

    return render(request, 'backoffice/token.html', {'token': api_token})

def support(request):
    if request.META.get("HTTP_HX_REQUEST") != 'true':
        return render(request, 'backoffice/support_full.html')

    return render(request, 'backoffice/support.html')

Maintenant dans votre template index.html, utilisez des blocks afin que la page index soit héritée par toutes les autres pages.

<!DOCTYPE html>
<html>
    <head>
        <script src="https://unpkg.com/htmx.org@1.2.1"></script>
    </head>

    <body>
        <progress id="content-loader" class="htmx-indicator" max="100"></progress>
        <aside>
            <ul>
                <li><a hx-get="{% url 'home' %}"
                        hx-push-url="true" hx-target="#content" hx-swap="innerHTML"
                        hx-indicator="#content-loader">Home</a></li>
                <li><a hx-get="{% url 'token' %}" hx-push-url="true"
                        hx-target="#content" hx-swap="innerHTML" 
                        hx-indicator="#content-loader">Token</a></li>
            </ul>
        </aside>
        <div id="content">{% block content %}{% endblock %}</div>
    <body>
</html>

Votre template token.html est le même qu’auparavant mais maintenant vous devez ajouter un second template appelé token_full.html dans le cas où la page est rechargée manuellement :


{% extends "home/index.html" %}

{% block content %}
    {% include "home/token.html" %}
{% endblock %}

Pareil pour support.html, ajoutez un fichier support_full.html :


{% extends "home/index.html" %}

{% block content %}
    {% include "home/support.html" %}
{% endblock %}

En gros l’on étend le template index.html afin de construire la page entière d’un coup côté serveur.

Il s’agit d’un petit bidouillage mais ce n’est pas bien méchant, et un middleware peut même être créé pour l’occasion afin de simplifier encore les choses.

Quoi d’autre ?

Nous avons seulement effleuré la surface de htmx. Cette lib (ou framework ?) inclut des tas de fonctionnalités utiles telles que :

  • Vous pouvez utiliser le verbe HTTP que vous voulez pour vos requêtes. Utilisez hx-get pour GET, hx-post pour POST, etc.
  • Vous pouvez utiliser du polling, des websockets, et des server side events, afin d’écouter les évènements provenant du serveur
  • Vous pouvez n’utiliser qu’une partie du fragment HTML retourné par le serveur (hx-select)
  • Vous pouvez utiliser des transitions CSS
  • Vous pouvez facilement travailler sur les formulaires et l’upload de fichier
  • Vous pouvez utiliser l’hyperscript de htmx, qui est un pseudo-language javascript pouvant facilement être intégré aux tags HTML pour un usage avancé

Conclusion

Je suis enthousiasmé par cette lib htmx comme vous pouvez le voir, et j’espère qu’un nombre croissant de gens va réaliser qu’ils n’ont pas nécessairement besoin d’un énorme framework JS pour leur projet.

Pour le moment j’ai uniquement intégré htmx à des bases de code de taille modeste en production, mais je suis persuadé que htmx s’intègre aussi parfaitement aux projets de grande envergure. Pour le moment je trouve le code facile à maintenir, léger, et son intégration facile à des frameworks backend comme Django est un must !

Si certains d’entre vous utilisent htmx en production, j’adorerais entendre vos retour sur le sujet !

Also available in English