Back to "HTMX (e un po' Alpine.js) per un'esperienza simile alla SPA in 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 (e un po' Alpine.js) per un'esperienza simile alla SPA in ASP.NET Core

Sunday, 15 September 2024

Introduzione

In questo sito uso HTMX ampiamente, questo è un modo super facile per rendere il vostro sito sentire più reattivo e più liscia senza dover scrivere un sacco di JavaScript.

NOTA: Non sono ancora del tutto soddisfatto di questo, HTMX ha alcuni elementi che rendono questo difficile

HDMX e ASP.NET Core

Utilizzando HTMX con ASP.NET Core è un po 'di un sogno, Ho scritto su questo in precedenza in questo post. E 'davvero abbastanza facile 'HTMXify' il tuo sito, ed è un ottimo modo per rendere il vostro sito sentire più reattivo.

Collegamenti normali

Come parte del mio ultimo andare in giro utilizzando HTMX ho deciso di fare il maggior numero di collegamenti su questo sito utilizzare HTMX come possibile. Quindi, ad esempio, se si fa clic sul pulsante 'Home' sopra di esso ora utilizza HTMX per caricare il contenuto della pagina.

Questo lo raggiungo semplicemente aggiungendo alcune proprietà al link tag, come questo:

    <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>

Qui potete vedere che aggiungo il hx-swap attributo, questo dice a HTMX di scambiare il contenuto dell'elemento di destinazione con il contenuto restituito dal server.

NOTA: NON usare hx-pushurl in quanto questo farà sì che il browser per ricaricare la pagina sul backbutton due volte.

La hx-target attributo indica a HTMX dove mettere il contenuto restituito dal server. In questo caso è il #contentcontainer elemento (dove viene caricato tutto il contenuto della pagina).

Aggiungo anche: hx-swap="show:window:top che rende la finestra scorrere verso l'inizio della pagina quando il contenuto viene scambiato.

La parte C# di questo è abbastanza semplice, semplicemente duplica il mio normale metodo Index ma invece carica la vista parziale. Il codice è mostrato di seguito. Ancora una volta questo usa HTMX.NET per rilevare la hx-request intestazione e restituire la vista parziale. C'è anche un cut-out per restituire la vista normale se la richiesta non è una richiesta 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);
    }

Il risultato è che questo link (e altri utilizzando la stessa tecnica) carica come 'AJAX' richieste e il contenuto viene scambiato senza una pagina completa ricaricare. Questo evita il fastidioso 'chiaro flash' si ottiene quando una pagina si ricarica a causa di come funziona il mio scambio di tema.

Quindi il metodo qui sotto può essere fatto funzionare comunque è super complicato e un po 'imbarazzante. A HTMX piace molto lavorare usando l'approccio basato sugli attributi. Uso htmx.ajax è tipo del metodo'se tutto il resto fallisce' e significa che è necessario incasinare con la storia borwser. Purtroppo la cronologia del browser usando pushState replaceState e popstate è davvero un po 'di un dolore al collo (ma può essere super potente per i casi di bordo).

Invece quello che sto facendo ora è solo aggiungere hx-boost al tag che contiene il contenuto del 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>

Questo sembra funzionare bene come hx-boost possono essere inseriti sui metodi genitori e si applicano a tutti i collegamenti bambini. E 'anche più standard HTMX senza scherzare con la storia ecc.

Nella rara occasione che non si desidera utilizzare HTMX per particolari collegamenti è possibile aggiungere hx-boost="false" al collegamento.

Tuttavia un problema con questo approccio è stato il mio blog link come questo: HTMX per un'esperienza simile alla SPA che non sono collegamenti "normali." Questi collegamenti sono generati dal parser markdown; mentre posso scrivere un'estensione MarkDig per aggiungere gli attributi HTMX ai collegamenti, ho deciso di usare un approccio diverso (perché ho già una tonnellata di contenuti che non voglio riparlare).

Invece ho aggiunto una funzione JavaScript che cerca tutti questi tipi di link, quindi utilizza htmx.ajax per fare lo scambio. Questo è essenzialmente ciò che HTMX fa comunque solo'manuale'.

Come si può vedere questa è solo una semplice funzione che cerca tutti i link nel div.prose elemento che inizia con un / e poi aggiunge un ascoltatore di eventi a loro. Quando il collegamento è cliccato l'evento è impedito, l'URL viene estratto e quindi il htmx.ajax funzione è chiamata con l'URL e l'elemento di destinazione da aggiornare.

(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);

Inoltre una volta che lo swap è stato fatto poi spingere il nuovo URL alla cronologia del browser e scorrere verso l'alto della pagina. Uso questo approccio in alcuni luoghi (come la ricerca) per dare una bella esperienza liscia burrosa.

Pulsante posteriore (questo è difficile per ottenere il lavoro)

Il tasto posteriore in questo tipo di app può essere problematico; TANTI 'propri' SPAs o disattivare il tasto posteriore o mettere in su con questo comportamento errato. Comunque volevo assicurarmi che il tasto posteriore funzionasse come previsto. Nota: Per far funzionare il tasto posteriore come previsto abbiamo anche bisogno di ascoltare per il popState evento. Questo evento viene lanciato quando l'utente naviga indietro o in avanti nella cronologia del browser. Quando questo evento viene licenziato possiamo ricaricare il contenuto dell'URL corrente.

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'
  });
 });
});

In questo codice si impedisce il comportamento predefinito del browser popstate con il event.preventDefault() Chiama. Poi estraiamo l'URL corrente dall' window.location.href proprietà ed eseguire una richiesta HTMX AJAX per caricare il contenuto per lo stato attuale. Una volta che il contenuto è stato caricato scorriamo in cima alla pagina.

Come accennato in precedenza non è possibile utilizzare anche hx-pushurl in quanto questo farà sì che il browser per ricaricare la pagina sul backbutton due volte.

HTMX più in generale (e Alpine.js)

Come avrete indovinato, sono un po' un fan di HTMX, combinato con ASP.NET Core e una mattering di Alpine.js che permette ai devs di creare un'esperienza utente davvero piacevole con il minimo sforzo. La cosa principale che preferisco rispetto a quelli più 'completamente formati' frameworks lato client come React / Angolar ecc... è che mi dà ancora un completo server-side rendering pipeline con ASP.NET Core, ma la 'feel' di una SPA. Alpine.js consente una semplice interattività lato client, una caratteristica molto semplice che ho aggiunto recentemente è stato rendere le mie categorie 'nascosto', ma espandere su un click.

Usare Alpine.js per questo significa che non è richiesto nessun JavaScript aggiuntivo.

        <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>

Qui potete vedere che uso il x-data attributo per creare un nuovo oggetto dati Alpine.js, in questo caso showCategories. Questo è un booleano che è agitato quando il h4 elemento è cliccato. La x-on:click l'attributo è usato per legare l'evento click alla funzione toggle.

All'interno dell'elenco delle categorie I poi utilizzare il x-show attributo per mostrare o nascondere l'elenco delle categorie in base al valore di showCategories. Io uso anche il x-transition attributo per aggiungere un effetto slide down piacevole quando le categorie sono mostrate o nascoste. La x-cloak attributo è usato per evitare che il 'flicker' delle categorie mostrate prima che il JavaScript sia in esecuzione. Ho una piccola classe CSS definita per questo:

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

In conclusione

Così è, un breve articolo su come utilizzare HTMX e un punto di Alpine.js per rendere il vostro sito più reattivo e 'SPA-like'. Spero che abbiate trovato questo utile, se avete domande o commenti non esitate a lasciarli qui sotto.

logo

©2024 Scott Galloway