Hotwire : Tailwind CSS, Htmx et Alpine.js

nora.nckm.eu

Illustration
Table des matières

The Hypermedia Driven Application (HDA) architecture is a synthesis of two preceding architectures: the original Multi-Page Application (MPA) architecture and the (relatively) newer Single-Page Application (SPA) architecture. It attempts to capture the advantages of both: the simplicity and reliability of MPAs, with a REST-ful Architecture that uses Hypermedia As The Engine Of Application State, while providing a better user experience, on par with SPAs in many cases.

Hotwire : HTML over the wire

Plutôt que de mettre en place un framework Javascript complexe tel que React, Vue, Angular, il est tout à fait possible de gérer le frontend d'une application web avec des technologies beaucoup plus simples. Les outils présentés ici permettent de créer des interfaces modernes, maintenables par un développeur seul ou une petite équipe. Ils sont utilisables directement en HTML, le but étant d'abstraire le Javascript et le CSS.

HTML + Tailwind CSS + Htmx + Alpine.js = ❤️

L'idée est de revenir aux technologies de base du web que sont HTML et CSS afin de concevoir des sites plus réactifs, plus écoresponsables et plus durables. L'idéal étant de faire des sites statiques en optimisant les requêtes et les échanges de données avec le serveur et en limitant l'utilisation du Javascript.

HTML + Simple.css = ❤️❤️

Dans une démarche plus radicale, le protocole Gemini est encore plus minimaliste.

Gemtext = ❤️❤️❤️

Tailwind CSS

Tailwind est un framework CSS basé sur des classes utilitaires plutôt que des classes par composant.

Les concepts du CSS doivent être assimilés puisqu'on les retrouve dans Tailwind. Les layouts flexbox et grid permettant de positionner les éléments sont particulèrement importants.

Installation

La procédure d'installation détaillée ici concerne Django. Pour l'installation dans un autre environnement se référer à la documentation officielle.

Module django-tailwind

Le module django-tailwind nécessite l'installation préalable de Node.js.

1. Installation du module :

pip install django-tailwind

2. Ajout de l'app tailwind dans settings.xml :

INSTALLED_APPS = [
    ...
    'tailwind',
]

3. Initialisation de Tailwind : une configuration et une feuille de style minimales sont créées dans theme/static_src/tailwind.config.js et theme/static_src/src/style.css.

python manage.py tailwind init

4. Ajout de l'app theme dans settings.xml :

INSTALLED_APPS = [
    ...
    'tailwind',
    'theme',
]

TAILWIND_APP_NAME = 'theme'
INTERNAL_IPS = [
    "127.0.0.1",
]

5. Installation des dépendance de Tailwind : installe les dépendances Javascript avec npm.

python manage.py tailwind install

6. Démarrage de Tailwind : surveille les modifications de classes dans les fichiers HTML et génère à la volée la feuille de style dans theme/static/css/dist/styles.css.

python manage.py tailwind start

7. Déploiement de Tailwind : compile et minimise la feuille de style pour la production dans theme/static/css/dist/styles.css.

python manage.py tailwind build

8. Ajout de la feuille de style Tailwind dans le template de base :

{% load tailwind_tags %}
...
<head>
    ...
    {% tailwind_css %}
    ...
</head>

Module pytailwindcss

Le module pytailwindcss ne dépend pas de Node.js, il utilise le client standalone officiel.

1. Installation du module :

pip install pytailwindcss

2. Téléchargement du client :

tailwindcss

3. Initialisation de la configuration tailwind.config.js :

tailwindcss init

4. Initialisation de la feuille de style minimale tailwind.css :

@tailwind base;
@tailwind components;
@tailwind utilities;

5. Démarrage de Tailwind : surveille les modifications de classes dans les fichiers HTML et génère à la volée la feuille de style.

tailwindcss -i tailwind.css -o static/style.css --watch

6. Déploiement de Tailwind : compile et minimise la feuille de style pour la production.

tailwindcss -i tailwind.css -o static/style.css --minify

7. Ajout de la feuille de style Tailwind dans le template de base :

{% load static %}
...
<head>
    ...
    <link rel="stylesheet" href="{% static 'style.css' %}">
    ...
</head>

Le fichier tailwind.config.js contient la configuration : fichiers analysés, theme, plugins…

Le fichier tailwind.css contient les styles personnalisés : directives @tailwind, @layer, @apply

Utilisation

Les sources sont disponibles sur sourcehut.

Il suffit d'utiliser les classes utilitaires fournies par Tailwind directement dans les fichiers HTML. Il en existe de nombreuses qui sont listées dans la documentation. Voici quelques exemples :

<body class="bg-gray-100 flex flex-col xl:flex-row">
    <h1 class="text-blue-400">Lorem ipsum</h1>
    <p class="text-white bg-indigo-900">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
    <input type="text" class="bg-transparent border border-red-600" placeholder="…">

    <p class="text-md font-mono font-bold">Hello World</p>
    <p class="text-lg font-sans line-through">Hello World</p>
    <p class="text-4xl font-serif text-center">Hello World</p>
    <p class="text-6xl font-mono text-right italic font-extrabold">Hello World</p>

    <div class="bg-red-700 h-10 w-auto mr-10"></div>
    <div class="bg-green-700 h-16 w-4/6 my-10"></div>
    <div class="bg-blue-700 h-24 w-6/12 mx-auto"></div>
    <div class="border border-pink-400 h-40 w-56 mt-10 mx-auto">
        <h2 class="text-pink-700 p-16">Lorem ipsum</h2>
    </div>

    <nav class="flex justify-between items-center px-16 bg-purple-800 text-white h-24">
        <h1>Title</h1>
        <ul class="flex justify-between w-56">
            <a href="#">
                <li>Link</li>
            </a>
            <a href="#">
                <li>Link</li>
            </a>
            <a href="#">
                <li>Link</li>
            </a>
        </ul>
    </nav>

    <div class="h-32 w-32 mt-16 mx-auto lg:bg-orange-500 md:bg-red-500 sm:bg-purple-800"></div>
    <div class="h-32 w-32 mt-16 mx-auto lg:bg-orange-500 md:bg-green-800 sm:bg-indigo-300"></div>
    <div class="h-32 w-32 mt-16 mx-auto lg:bg-orange-500 md:bg-blue-200 sm:bg-teal-600"></div>
</body>

Astuce : il existe des bibliothèques de composants prêts à l'emploi : TailwindUI, daisyUI, Tailblocks.

Simple.css

Simple.css est un framework CSS sans classe. Il permet de créer très rapidement des sites simples tels que des blogs ou des pages de présentation. Il ne contient pas de classes CSS, on code uniquement avec du HTML sémantique tout en profitant de fonctionnalités améliorées :

La feuille de style est très légère et peut être étendue en ajoutant une feuille de style personnalisée dans le head de la page :

<link rel="stylesheet" href="simple.min.css">
<link rel="stylesheet" href="simple.sub.css">

Le présent site utilise Simple.css et des pages HTML classiques.

Htmx

Htmx est une bibliothèque donnant accès à des fonctionnalités modernes du navigateur (AJAX, CSS Transitions, WebSockets, Server Sent Events) directement en HTML plutôt qu'en Javascript.

Installation

L'installation consiste à copier la librairie et l'inclure dans le head de la page avec une balise script :

<script defer src="/path/to/htmx.min.js"></script>

La configuration peut ensuite se faire avec une balise meta :

<meta name="htmx-config" content='{"defaultSwapStyle":"outerHTML"}'>

Se référer à la documentation et à la référence.

Utilisation

Htmx envoie des requêtes AJAX et attend en retour des réponses HTML (fragments HTML). Htmx échange ensuite le HTML retourné avec la cible indiquée dans la page.

Request : définition de la requête (type et URL).

<!-- hx-get, hx-post, hx-put, hx-patch, hx-delete -->
<a hx-get="/link">Link</a>

Trigger : évènement déclenchant la requête.

<!-- hx-trigger -->
<div hx-get="/clicked" hx-trigger="click">Click Me</div>
<div hx-get="/clicked" hx-trigger="click[ctrlKey]">Click Me</div>
<input hx-get="/search" hx-trigger="keyup changed delay:500ms"/>
<div hx-get="/news" hx-trigger="every 3s"></div>
<div hx-get="/messages" hx-trigger="load delay:1s"></div>

Indicator : indicateur de requête en cours (icône de chargement).

<!-- hx-indicator -->
<button hx-get="/click">
    Click Me!
    <img class="htmx-indicator" src="/spinner.svg">
</button>

<button hx-get="/click" hx-indicator="#indicator">
    Click Me!
</button>
<img id="indicator" class="htmx-indicator" src="/spinner.svg"/>

Target : élément du DOM qui sera remplacé par le HTML retourné (élément portant la requête par défaut).

<!-- hx-target -->
<a hx-get="/link" hx-target="#results">Link</a>
<div id="results"></div>

Swapping : stratégie de remplacement entre la réponse et la cible d'une requête.

<!-- hx-swap -->
<div hx-get="/example" hx-swap="afterend"></div>
<div hx-get="/example" hx-swap="innerHTML swap:1s"></div>
<div hx-get="/example" hx-swap="beforeEnd scroll:bottom"></div>
<div hx-get="/example" hx-swap="innerHTML show:top" hx-target="#another-div"></div>

Synchronization : synchronisation de requêtes AJAX entre plusieurs éléments.

<!-- hx-sync -->
<form hx-post="/store">
    <input id="title" name="title" type="text"
        hx-post="/validate"
        hx-trigger="change"
        hx-sync="closest form:abort">
    <button type="submit">Submit</button>
</form>

Paramètre : paramètres inclus dans la requête. Par défaut, la valeur de l'élément ou les inputs d'un formulaire sont inclus.

Pour filtrer ou ajouter des paramètres à une requête :

<!-- hx-include, hx-params, hx-vals -->
<div hx-get="/example" hx-params="*"></div>
<div hx-get="/example" hx-vals='{"myVal": "My Value"}'>

<button hx-post="/send" hx-include="[name='email']">Send</button>
<input name="email" type="email"/>

Pour sélectionner les éléments d'une réponse qui remplaceront la cible :

<!-- hx-select, hx-select-oob -->
<button hx-get="/info" hx-select="#info-details" hx-swap="outerHTML">
    Get Info!
</button>

<div id="alert"></div>
<button hx-get="/info"
    hx-select="#info-details"
    hx-swap="outerHTML"
    hx-select-oob="#alert">
    Get Info!
</button>

Confirmation : fenêtre de confirmation

<!-- hx-confirm -->
<button hx-delete="/account" hx-confirm="Are you sure you wish to delete your account?">
    Delete My Account
</button>

Héritage : les attributs héritables s'appliquent à l'élément qui les porte ainsi qu'aux éléments enfants.

<div hx-confirm="Are you sure?">
    <button hx-delete="/account">Delete</button>
    <button hx-put="/account">Update</button>
    <button hx-get="/" hx-confirm="unset">Cancel</button>
</div>

Boost : conversion des liens et formulaires HTML classiques en requêtes AJAX ayant pour cible le body de la page.

<!-- hx-boost -->
<div hx-boost="true">
    <a href="/link">Link</a>
</div>

Historique : ajoute une URL dans l'historique du navigateur.

<!-- hx-push-url -->
<a hx-get="/link" hx-push-url="true">Link</a>

WebSockets et Server Sent Events :

<!-- hx-ws, hx-sse -->
<div hx-ws="connect:wss:/chatroom">
    <div id="chat_room">
        ...
    </div>
    <form hx-ws="send:submit">
        <input name="chat_message">
    </form>
</div>

<body hx-sse="connect:/news_updates">
    <div hx-get="/news" hx-trigger="sse:new_news"></div>
</body>

Astuce : les fragments de gabarit permettent de regrouper des gabarits partiels dans un seul fichier : django-template-partials, jinja2-fragments.

Alpine.js

Alpine.js est un framework Javascript minimaliste utilisable en HTML.

Installation

L'installation consiste à copier la librairie et l'inclure dans le head de la page avec une balise script :

<script defer src="/path/to/alpine.min.js"></script>

Se référer à la documentation.

Utilisation

Alpine.js est un framework réactif dans le sens où lorsqu'une donnée est modifiée, tout ce qui dépend de cette donnée "réagit" automatiquement à ce changement.

Exemple de déclaration d'une donnée, déclenchement d'un évènement et réaction au changement de la donnée :

<div x-data="{ open: false }">
    <button @click="open = true">Expand</button>
    <span x-show="open">
        Content...
    </span>
</div>

Principaux attributs :

x-data      Declare data for a block of HTML
x-bind      Set HTML attributes on an element
x-on        Listen events on an element
x-text      Set the text content of an element
x-html      Set the inner HTML of an element
x-model     Synchronize data with an input element
x-show      Toggle the visibility of an element
x-for       Repeat a block of HTML based on a data set
x-if        Conditionally add/remove a block of HTML
x-init      Run code when an element is initialized
x-effect    Execute a script each time one of its dependancies change
x-ref       Reference an element by key

Principales propriétés :

$store      Access a global store registered using Alpine.store()
$el         Reference the current DOM element
$dispatch   Dispatch a custom event from the current element
$watch      Watch data and run the provided callback anytime it changes
$refs       Access an element by key referenced by x-ref

Principales méthodes :

Alpine.data()   Define a data object used by x-data
Alpine.store()  Define a global data accessed using $store

Pagefind

Pagefind est une bibliothèque de recherche statique optimisée pour utiliser le moins de bande passante possible. Contrairement à Lunr.js qui construit un index de recherche unique, Pagefind le divise en fragments ordonnés. D'autres outils de recherche en texte intégral (full-text search) sont listés dans la documentation de Hugo.

Construction de l'index de recherche

Télecharger et exécuter le binaire précompilé pagefind :

./pagefind --site public

Affichage des résultats de recherche

Insérer les scripts CSS et Javascript suivants dans une page :

<link href="/pagefind/pagefind-ui.css" rel="stylesheet">
<script src="/pagefind/pagefind-ui.js"></script>
<div id="search"></div>
<script>
    window.addEventListener('DOMContentLoaded', (event) => {
        new PagefindUI({ element: "#search", showSubResults: true });
    });
</script>

Prévoir une méthode alternative dans la cas où le Javascript serait désactivé (recherche DuckDuckGo par exemple) :

<noscript>
  <form action="https://duckduckgo.com/" target="_blank" rel="noopener">
    <input type="hidden" name="sites" value="nora.nckm.eu">
    <input type="hidden" name="ko" value="-2">
    <input type="hidden" name="k1" value="-1">
    <input type="hidden" name="kz" value="-1">
    <input type="hidden" name="km" value="m">
    <input type="hidden" name="kae" value="d">
    <input type="hidden" name="k7" value="#212121">
    <label for="q">DuckDuckGo</label>
    <input type="search" name="q" id="q">
    <input type="submit" value="Rechercher">
  </form>
</noscript>
Emojis

Un commentaire sur un de mes articles ? Commencez une discussion sur ma liste de diffusion en envoyant un email à ~nora/public-inbox@lists.sr.ht [règles]