Back to "HTMX (et un peu Alpine.js) pour une expérience de type SPA dans ASP.NET Core"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

Alpine.js ASP.NET HTMX

HTMX (et un peu Alpine.js) pour une expérience de type SPA dans ASP.NET Core

Sunday, 15 September 2024

Présentation

Sur ce site j'utilise HTMX largement, c'est une façon super facile de rendre votre site plus réactif et plus lisse sans avoir à écrire beaucoup de JavaScript.

REMARQUE: Je ne suis pas encore tout à fait satisfait de cela, HTMX a quelques éléments qui rendent cette délicate

HTMX et ASP.NET Core

Utilisation de HTMX avec ASP.NET Core est un peu un rêve, j'ai écrit à ce sujet précédemment dans ce posteC'est ce que j'ai dit. Il est vraiment assez facile de « HTMXify » votre site, et c'est une excellente façon de rendre votre site plus réactif.

Liens normaux

Dans le cadre de mon dernier tour en utilisant HTMX j'ai décidé de faire le plus de liens possible sur ce site utiliser HTMX. Ainsi, par exemple, si vous cliquez sur le bouton 'Home' au-dessus, il utilise maintenant HTMX pour charger le contenu de la page.

J'y parviens en ajoutant simplement quelques propriétés à la balise link, comme ceci:

    <a asp-action="IndexPartial" asp-controller="Home" hx-swap="show:window:top"  hx-target="#contentcontainer" hx-boost="true" class="mr-auto sm:mr-6">
     <div class="svg-container">
        <img src="/img/logo.svg" asp-append-version="true"  width="180px" height="30px"  alt="logo"  :class="{ 'img-filter-dark': isDarkMode }"/>
     </div>
    </a>

Ici vous pouvez voir que j'ajoute le hx-swap attribut, cela demande à HTMX d'échanger le contenu de l'élément cible avec le contenu retourné du serveur.

REMARQUE: NE PAS utiliser hx-pushurl car cela va provoquer le navigateur de recharger la page sur le bouton arrière deux fois.

Les hx-target attribut indique à HTMX où mettre le contenu retourné depuis le serveur. Dans ce cas, c'est le #contentcontainer élément (où tout le contenu de la page est chargé).

J'ajoute également: hx-swap="show:window:top qui fait défiler la fenêtre vers le haut de la page lorsque le contenu est échangé.

La partie C# de ceci est assez simple, il suffit de dupliquer ma méthode d'index normale, mais charge plutôt la vue partielle. Le code est indiqué ci-dessous. Encore une fois, cela utilise HTMX.NET pour détecter les hx-request header et retourner la vue partielle. Il y a aussi une coupure pour retourner la vue normale si la requête n'est pas une requête HTMX.

    [Route("/IndexPartial")]
    [OutputCache(Duration = 3600, VaryByHeaderNames = new[] { "hx-request" })]
    [ResponseCache(Duration = 300, VaryByHeader = "hx-request",
        Location = ResponseCacheLocation.Any)]
    [HttpGet]
    public async Task<IActionResult> IndexPartial()
    {
        ViewBag.Title = "mostlylucid";
        if(!Request.IsHtmx()) return RedirectToAction("Index");
        var authenticateResult = await GetUserInfo();
        var posts = await BlogService.GetPagedPosts(1, 5);
        posts.LinkUrl = Url.Action("Index", "Home");
     
        var indexPageViewModel = new IndexPageViewModel
        {
            Posts = posts, Authenticated = authenticateResult.LoggedIn, Name = authenticateResult.Name,
            AvatarUrl = authenticateResult.AvatarUrl
        };
        return PartialView("_HomePartial", indexPageViewModel);
    }

Le résultat est que ce lien (et d'autres utilisant la même technique) charge que les requêtes « AJAX » et le contenu est échangé sans recharger toute la page. Cela évite le « flash de lumière » agaçant que vous obtenez quand une page recharge en raison de la façon dont mon thème de commutation fonctionne.

Donc la méthode ci-dessous peut être faite pour fonctionner mais c'est super difficile et un peu gênant. HTMX aime vraiment travailler en utilisant l'approche basée sur les attributs. Utilisation htmx.ajax est une sorte de méthode "si tout le reste échoue" et signifie que vous devez gâcher avec l'histoire de borwser. Malheureusement, l'historique du navigateur en utilisant pushState replaceState et popstate est vraiment un peu de douleur dans le cou (mais peut être super puissant pour les cas de bord).

Au lieu de cela ce que je fais maintenant c'est juste ajouter hx-boost à l'étiquette qui contient du contenu de blog.

   <div class="prose prose max-w-none border-b py-2 text-black dark:prose-dark sm:py-2" hx-boost="true"  hx-target="#contentcontainer">
        @Html.Raw(Model.HtmlContent)
    </div>

Il semble que cela fonctionne bien. hx-boost peuvent être placés sur les méthodes parentales et s'appliquer à tous les liens d'enfant. Il est également plus standard HTMX sans se mêler à l'histoire etc.

Dans la rare occasion que vous ne voulez pas utiliser HTMX pour des liens particuliers, vous pouvez simplement ajouter hx-boost="false" vers le lien.

Cependant, un problème avec cette approche était mes liens de blog comme celui-ci: HTMX pour une expérience de type SPA qui ne sont pas des liens « normaux ». Ces liens sont générés par l'analyseur de balisage; tandis que j'écris une extension MarkDig pour ajouter les attributs HTMX aux liens, j'ai décidé d'utiliser une approche différente (parce que j'ai déjà une tonne de contenu que je ne veux pas re-parser).

Au lieu de cela, j'ai ajouté une fonction JavaScript qui recherche tous ces types de lien, puis utilise htmx.ajax pour faire l'échange. C'est essentiellement ce que fait HTMX de toute façon simplement « manuel ».

Comme vous pouvez le voir, c'est juste une fonction simple qui recherche tous les liens dans le div.prose élément qui commence par un / puis ajoute un auditeur d'événement à eux. Lorsque le lien est cliqué, l'événement est empêché, l'URL est extraite, puis le htmx.ajax fonction est appelée avec l'URL et l'élément cible à mettre à jour.

(function(window) {
   
    window.blogswitcher =() =>  {
        const blogLinks = document.querySelectorAll('div.prose  a[href^="/"]');

        // Iterate through all the selected blog links
        blogLinks.forEach(link => {
            link.addEventListener('click', function(event) {
               event.preventDefault();
                let link = event.currentTarget;
                let url = link.href;
                htmx.ajax('get', url, {
                    target: '#contentcontainer',  // The container to update
                    swap: 'innerHTML',            // Replace the content inside the target


                }).then(function() {
                    history.pushState(null, '', url);
                  
                        window.scrollTo({
                            top: 0,
                            behavior: 'smooth' // For a smooth scrolling effect
                        });
                    
                });
            });
        });
        
    };
})(window);

En outre, une fois que l'échange a été fait, je pousse ensuite la nouvelle URL à l'historique du navigateur et défile vers le haut de la page. J'utilise cette approche dans quelques endroits (comme la recherche) pour donner une belle expérience lisse au beurre.

Dos Button (C'est difficile de travailler)

Le bouton arrière de ce type d'application peut être problématique; MANY 'proper' SPAs soit désactiver le bouton arrière ou mettre en place avec ce comportement incorrect. Cependant, je voulais m'assurer que le bouton arrière fonctionnait comme prévu. Note: Pour que le bouton arrière fonctionne comme prévu, nous devons aussi écouter le popState l'événement. Cet événement est déclenché lorsque l'utilisateur navigue en arrière ou en avant dans l'historique du navigateur. Lorsque cet événement est lancé, nous pouvons recharger le contenu de l'URL actuelle.

window.addEventListener("popstate",   (event) => {
 // When the user navigates back, reload the content for the current URL
 event.preventDefault();
 let url = window.location.href;
 // Perform the HTMX AJAX request to load the content for the current state
 htmx.ajax('get', url, {
  target: '#contentcontainer',
  swap: 'innerHTML'
 }).then(function () {
  // Scroll to the top of the page
  window.scrollTo({
   top: 0,
   behavior: 'smooth'
  });
 });
});

Dans ce code, nous prévenons le comportement popstate navigateur par défaut avec le event.preventDefault() Appelez. Nous extrayons ensuite l'URL actuelle de la window.location.href property et effectuez une requête HTMX AJAX pour charger le contenu pour l'état courant. Une fois que le contenu a été chargé, nous faisons défiler vers le haut de la page.

Comme mentionné précédemment, vous ne pouvez pas utiliser ALSO hx-pushurl car cela provoquera le navigateur à recharger la page sur backbutton deux fois.

HTMX plus généralement (et Alpine.js)

Comme vous l'avez peut-être deviné je suis un peu un fan de HTMX, combiné avec ASP.NET Core et une touche d'Alpine.js il permet à devs de créer une expérience utilisateur vraiment agréable avec un minimum d'effort. La chose majeure que je préfère par rapport aux structures de côté client les plus 'complètement formées' comme React / Angular etc... est qu'il me donne toujours un pipeline de rendu côté serveur complet avec ASP.NET Core mais le'sens' d'un SPA. Alpine.js permet une simple interactivité côté client, une fonctionnalité très simple que j'ai ajoutée récemment était de faire mes catégories « cachées » mais étendre sur un clic.

Utiliser Alpine.js pour cela signifie qu'il n'y a pas de JavaScript supplémentaire requis.

        <div class="mb-8 mt-6 border-neutral-400 dark:border-neutral-600 border rounded-lg" x-data="{ showCategories: false }">
            <h4 
                class="px-5 py-1 bg-neutral-500  bg-opacity-10 rounded-lg  font-body text-primary dark:text-white w-full flex justify-between items-center cursor-pointer"
                x-on:click="showCategories = !showCategories"
            >
                Categories
                <span>
                    <i
                        class="bx text-2xl"
                        :class="showCategories ? 'bx-chevron-up' : 'bx-chevron-down'"
                    ></i>
                </span>
            </h4>
            <div 
                class="flex flex-wrap gap-2 pt-2 pl-5 pr-5 pb-2"
                x-show="showCategories" 
                x-cloak
                x-transition:enter="max-h-0 opacity-0"
                x-transition:enter-end="max-h-screen opacity-100"
                x-transition:leave="max-h-screen opacity-100"
                x-transition:leave-end="max-h-0 opacity-0"
            >
                @foreach (var category in ViewBag.Categories)
                {
                    <partial name="_Category" model="category"/>
                }
           
            </div>
        </div>

Ici vous pouvez voir que j'utilise le x-data attribut pour créer un nouvel objet de données Alpine.js, dans ce cas showCategoriesC'est ce que j'ai dit. C'est un booléen qui est frappé quand le h4 l'élément est cliqué. Les x-on:click attribut est utilisé pour lier l'événement de clic à la fonction toggle.

Dans la liste des catégories, j'utilise x-show attribut pour afficher ou masquer la liste des catégories basée sur la valeur de showCategoriesC'est ce que j'ai dit. J'utilise aussi les x-transition attribut pour ajouter un bel effet de glisser vers le bas lorsque les catégories sont affichées ou cachées. Les x-cloak attribut est utilisé pour empêcher le 'flicker' des catégories affichées avant que le JavaScript n'ait fonctionné. J'ai une petite classe CSS définie pour ceci:

[x-cloak] { display: none !important; }

En conclusion

Donc c'est tout, un court article sur la façon d'utiliser HTMX et un point d'Alpine.js pour rendre votre site plus réactif et 'comme SPA'. J'espère que vous l'avez trouvé utile, si vous avez des questions ou des commentaires, n'hésitez pas à les laisser ci-dessous.

logo

©2024 Scott Galloway