Back to "HTMX (och lite alpin.js) för en SPA-liknande upplevelse i 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

ASP.NET HTMX Apline.js

HTMX (och lite alpin.js) för en SPA-liknande upplevelse i ASP.NET Core

Sunday, 15 September 2024

Inledning

På denna webbplats använder jag HTMX Ordförande I stor utsträckning är detta ett super enkelt sätt att få din webbplats att känna sig mer lyhörd och smidigare utan att behöva skriva mycket JavaScript.

OBS: Jag är inte helt nöjd med detta ännu, HTMX har några element som gör detta svårt

HTMX och ASP.NET Core

Att använda HTMX med ASP.NET Core är lite av en dröm, jag har skrivit om detta tidigare i detta inlägg....................................... Det är verkligen ganska lätt att 'HTMXify' din webbplats, och det är ett bra sätt att få din webbplats att känna sig mer lyhörd.

Normala länkar

Som en del av min senaste gå runt med hjälp av HTMX bestämde jag mig för att göra så många länkar på denna webbplats använder HTMX som möjligt. Så till exempel om du klickar på "Home" knappen ovanför det nu använder HTMX för att ladda sidans innehåll.

Jag uppnår detta genom att helt enkelt lägga till några egenskaper till länktaggen, så här:

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

Här kan du se Jag lägger till hx-swap attribut, detta säger HTMX att byta innehållet i målelementet med det innehåll som returneras från servern.

OBS: Använd INTE hx-pushurl eftersom detta gör att webbläsaren laddar om sidan på backknappen två gånger.

I detta sammanhang är det viktigt att se till att hx-target attributet talar om för HTMX var innehållet som returneras från servern ska placeras. I det här fallet är det #contentcontainer element (där allt sidinnehåll är laddat).

Jag lägger också till hx-swap="show:window:top vilket gör att fönstret rullar till toppen av sidan när innehållet byts ut.

C# delen av detta är ganska enkel, det bara duplicerar min normala Index metod men istället laddar partiella vy. Koden visas nedan. Återigen använder detta HTMX.NET Ordförande för att upptäcka hx-request sidhuvud och returnera delvyn. Det finns också en cut-out för att returnera den normala vyn om begäran inte är en HTMX begäran.

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

Resultatet av detta är att denna länk (och andra använder samma teknik) belastning som "AJAX" förfrågningar och innehållet byts ut utan en hel sida omladdning. Detta undviker irriterande "ljusblixt" du får när en sida laddar om på grund av hur mitt tema byta fungerar.

Blogglänkar (det enklare sättet)

Så metoden nedan KAN göras för att fungera men det är super knepigt och lite pinsamt. HTMX gillar verkligen att arbeta med attributet baserat tillvägagångssätt. Användning htmx.ajax är typ av "om allt annat misslyckas" metoden och innebär att du måste röra till Borwser historia. Tyvärr webbläsarhistorik med hjälp av pushState replaceState och popstate är verkligen lite av en smärta i nacken (men kan vara super kraftfull för kant fall).

Istället vad jag gör nu är bara lägga till hx-boost till taggen som innehåller blogginnehåll.

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

Detta verkar fungera bra och hx-boost kan placeras på förälder metoder och tillämpas på alla barn länkar. Det är också mer standard HTMX utan att jävlas med historia etc.

I det sällsynta tillfälle som du inte vill använda HTMX för särskilda länkar kan du bara lägga till hx-boost="false" till länken.

Blogg Länkar (OBS: Jag övergav detta tillvägagångssätt men hålla det här för referens)

Men ett problem med detta tillvägagångssätt var min blogg länkar som denna: HTMX för en SPA-liknande upplevelse som inte är "normala" länkar. Dessa länkar genereras av markdown-tolken; medan jag kunde skriva en MarkDig-tillägg för att lägga till HTMX-attribut till länkarna, bestämde jag mig för att använda ett annat tillvägagångssätt (eftersom jag redan har massor av innehåll jag inte vill om-parse).

Istället lade jag till en JavaScript-funktion som letar efter alla dessa typer av länkar, sedan använder htmx.ajax För att göra bytet. Detta är i huvudsak vad HTMX gör ändå bara "manuell".

Som ni kan se är detta bara en enkel funktion som letar efter alla länkar i div.prose element som börjar med en / och sedan lägger en händelse lyssnare till dem. När länken klickas förhindras händelsen, URL extraheras och sedan htmx.ajax Funktionen kallas med webbadressen och målelementet att uppdatera.

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

Dessutom när bytet har gjorts trycker jag sedan på den nya URL:en till webbläsarens historik och bläddrar till toppen av sidan. Jag använder detta tillvägagångssätt på några ställen (som sökning) för att ge en fin smörig smidig upplevelse.

Ryggknapp (Detta är svårt att få arbete)

Bakknappen i denna typ av app kan vara problematisk; MÅNGA "riktiga" SPAs antingen inaktivera bakknappen eller stå ut med detta felaktiga beteende. Men jag ville se till att den bakre knappen fungerade som förväntat. Obs: För att få den bakre knappen att fungera som förväntat måste vi också lyssna på popState händelse. Denna händelse avfyras när användaren navigerar bakåt eller framåt i webbläsarens historik. När händelsen avfyras kan vi ladda om innehållet för den aktuella webbadressen.

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

I denna kod förhindrar vi standard webbläsare popstate beteende med event.preventDefault() Jag synar. Vi extraherar sedan den aktuella webbadressen från window.location.href egendom och utföra en HTMX AJAX begäran om att ladda innehållet för det aktuella tillståndet. När innehållet har laddats rullar vi till toppen av sidan.

Som tidigare nämnts kan du inte heller använda hx-pushurl eftersom detta kommer att göra att webbläsaren laddar om sidan på backknappen två gånger.

HTMX mer allmänt (och alpina.js)

Som du kanske har gissat är jag något en beundrare av HTMX, i kombination med ASP.NET Core och en fadder av Alpine.js det ger devs för att skapa en riktigt trevlig användarupplevelse med minimal ansträngning. Det viktigaste jag föredrar framför de mer "fullständigt formade" klientsidan ramar som React / Angular etc... är att det fortfarande ger mig en full server-side rendering pipeline med ASP.NET Core men "känner" av ett SPA. Alpine.js möjliggör enkel klient sida interaktivitet, en mycket enkel funktion som jag nyligen lagt till var att göra mina kategorier "dolda" men expandera på ett klick.

Att använda Alpine.js för detta innebär att det inte behövs något extra JavaScript.

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

Här kan du se att jag använder x-data attribut för att skapa ett nytt dataobjekt Alpine.js, i detta fall showCategories....................................... Detta är en boolean som är växlad när h4 elementet klickas. I detta sammanhang är det viktigt att se till att x-on:click egenskapen används för att binda klickhändelsen till funktionen Växla.

I listan över kategorier använder jag sedan x-show attribut för att visa eller dölja listan över kategorier baserat på värdet av showCategories....................................... Jag använder också x-transition attribut för att lägga till en fin bildnedslagseffekt när kategorierna visas eller döljs. I detta sammanhang är det viktigt att se till att x-cloak egenskap används för att förhindra att 'flicker' av kategorierna visas innan JavaScript har körts. Jag har en liten CSS klass definieras för detta:

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

Slutsatser

Så det är det, en kort artikel om hur man använder HTMX och en plats av Alpine.js för att få din webbplats att känna sig mer lyhörd och "SPA-liknande". Jag hoppas att du finner detta användbart, om du har några frågor eller kommentarer är du välkommen att lämna dem nedan.

logo

©2024 Scott Galloway