Back to "HTMX (en een beetje Alpine.js) voor een SPA-achtige ervaring 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 (en een beetje Alpine.js) voor een SPA-achtige ervaring in ASP.NET Core

Sunday, 15 September 2024

Inleiding

Op deze site gebruik ik HTMX uitgebreid, dit is een super gemakkelijke manier om uw site voelen meer responsief en gladder zonder te schrijven veel JavaScript.

OPMERKING: Ik ben hier nog niet helemaal blij mee, HTMX heeft een aantal elementen die dit lastig maken

HTMX en ASP.NET Core

Het gebruik van HTMX met ASP.NET Core is een beetje een droom, ik heb hierover eerder geschreven in dit bericht. Het is echt vrij gemakkelijk om 'HTMXify' uw site, en het is een geweldige manier om uw site voelen meer responsief.

Als onderdeel van mijn laatste rondgaan met behulp van HTMX heb ik besloten om zoveel mogelijk links te maken op deze site gebruik maken van HTMX mogelijk. Als u bijvoorbeeld op de knop 'Home' klikt, gebruikt u nu HTMX om de inhoud van de pagina te laden.

Ik bereik dit door simpelweg een aantal eigenschappen toe te voegen aan de link tag, zoals dit:

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

Hier kun je zien dat ik de hx-swap attribuut, dit vertelt HTMX om de inhoud van het doelelement te ruilen met de inhoud van de server.

OPMERKING: Gebruik geen hx-pushurl omdat dit ervoor zorgt dat de browser de pagina op de backbutton twee keer herlaadt.

De hx-target attribuut vertelt HTMX waar de inhoud die van de server wordt teruggestuurd moet worden geplaatst. In dit geval is het de #contentcontainer element (waar alle pagina-inhoud wordt geladen).

Ik voeg er ook aan toe: hx-swap="show:window:top waardoor het venster naar de bovenkant van de pagina scroll wanneer de inhoud wordt geruild.

Het C# deel hiervan is vrij eenvoudig, het dupliceert gewoon mijn normale Index methode maar laadt in plaats daarvan de gedeeltelijke weergave. De code staat hieronder. Opnieuw gebruikt dit HTMX.NET het detecteren van de hx-request kop en terug naar de gedeeltelijke weergave. Er is ook een cut-out om de normale weergave terug te geven als het verzoek niet een HTMX verzoek is.

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

Het resultaat hiervan is dat deze link (en anderen die dezelfde techniek gebruiken) geladen wordt als 'AJAX' en de inhoud verwisseld wordt zonder een volledige pagina opnieuw te laden. Dit vermijdt de vervelende 'light flash' die je krijgt wanneer een pagina herlaadt als gevolg van hoe mijn thema wisselen werkt.

Dus de methode hieronder KAN worden gemaakt om te werken maar het is super lastig en een beetje ongemakkelijk. HTMX houdt er echt van om te werken met behulp van de attribuut gebaseerde aanpak. Gebruik htmx.ajax is een soort van de 'als al het andere faalt' methode en betekent dat je moet knoeien met borwser geschiedenis. Helaas browser geschiedenis met behulp van pushState replaceState en popstate is echt een beetje een pijn in de nek (maar kan super krachtig zijn voor rand gevallen).

In plaats daarvan wat ik nu doe is gewoon toevoegen hx-boost naar de tag die bloginhoud bevat.

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

Dit lijkt goed te werken als hx-boost kan op oudermethoden worden geplaatst en op alle kinderlinks worden toegepast. Het is ook meer standaard HTMX zonder te knoeien met geschiedenis etc.

In de zeldzame gelegenheid dat u don't wilt gebruiken HTMX voor bepaalde links kunt u gewoon toevoegen hx-boost="false" naar de link.

Echter een probleem met deze aanpak was mijn blog links zoals deze: HTMX voor een SPA-achtige ervaring die geen 'normale' links zijn. Deze links worden gegenereerd door de markdown parser; terwijl ik een MarkDig-extensie kan schrijven om de HTMX-attributen aan de links toe te voegen, heb ik besloten om een andere aanpak te gebruiken (omdat ik al een ton content heb die ik niet wil repareren).

In plaats daarvan heb ik een JavaScript functie toegevoegd die op zoek is naar al deze soorten link, dan gebruikt htmx.ajax om de ruil te doen. Dit is in wezen wat HTMX sowieso gewoon 'handmatig' doet.

Zoals je kunt zien is dit slechts een eenvoudige functie die op zoek is naar alle links in de div.prose element dat begint met een / en dan een luisteraar aan hen toevoegt. Wanneer de link wordt geklikt wordt de gebeurtenis voorkomen, de URL wordt uitgepakt en vervolgens de htmx.ajax functie wordt aangeroepen met de URL en het doelelement om te updaten.

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

Bovendien zodra de swap is gemaakt duw ik de nieuwe URL naar de browser geschiedenis en scroll naar de bovenkant van de pagina. Ik gebruik deze aanpak op een paar plaatsen (zoals zoeken) om een lekkere boterzachte ervaring te geven.

Back Button (dit is lastig om te werken)

De achterknop in dit type app kan problematisch zijn; VEEL 'eigen' SPA's schakelen de achterknop uit of houden zich aan dit onjuiste gedrag. Maar ik wilde ervoor zorgen dat de achterknop werkte zoals verwacht. Opmerking: Om de achterknop aan het werk te krijgen zoals verwacht moeten we ook luisteren voor de popState event. Deze gebeurtenis wordt afgevuurd wanneer de gebruiker terug of vooruit navigeert in de browsergeschiedenis. Wanneer dit evenement wordt afgevuurd kunnen we de inhoud van de huidige URL herladen.

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 deze code voorkomen we het standaard browser popstate gedrag met de event.preventDefault() Call. Vervolgens halen we de huidige URL uit de window.location.href eigenschap en voer een HTMX AJAX verzoek uit om de inhoud voor de huidige toestand te laden. Zodra de inhoud is geladen scrollen we naar de bovenkant van de pagina.

Zoals eerder vermeld kunt u OOK niet gebruiken hx-pushurl omdat dit ervoor zorgt dat de browser de pagina op de backbutton twee keer herlaadt.

HTMX meer in het algemeen (en Alpine.js)

Zoals je misschien hebt geraden ben ik een beetje een fan van HTMX, gecombineerd met ASP.NET Core en een mattering van Alpine.js het machtigt devs om een echt leuke gebruikerservaring met minimale inspanning te creëren. Het belangrijkste dat ik verkies boven de meer 'volledig gevormde' client side frameworks zoals React / Angular etc...is dat het me nog steeds een volledige server-side rendering pipeline met ASP.NET Core maar het 'gevoel' van een SPA geeft. Alpine.js maakt eenvoudige interactiviteit van klantenzijde mogelijk, een zeer eenvoudige functie die ik onlangs heb toegevoegd was het maken van mijn categorieën'verborgen' maar uitbreiden met een klik.

Het gebruik van Alpine.js betekent dat er geen extra JavaScript nodig is.

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

Hier kun je zien dat ik gebruik maak van de x-data attribuut om een nieuw Alpine.js dataobject aan te maken, in dit geval showCategories. Dit is een booleaan die aan het toggled wanneer de h4 element is geklikt. De x-on:click attribuut wordt gebruikt om de klik gebeurtenis te binden aan de toggle functie.

In de lijst van categorieën I dan gebruik de x-show attribuut om de lijst van categorieën te tonen of te verbergen op basis van de waarde van showCategories. Ik gebruik ook de x-transition attribuut om een mooi slide-down effect toe te voegen wanneer de categorieën worden getoond of verborgen. De x-cloak attribuut wordt gebruikt om te voorkomen dat de 'flicker' van de categorieën wordt getoond voordat de JavaScript draait. Ik heb hier een beetje CSS klasse voor gedefinieerd:

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

Conclusie

Dus dat is het, een kort artikel over het gebruik van HTMX en een plekje Alpine.js om uw site meer responsief en 'SPA-achtig' te laten voelen. Ik hoop dat u dit nuttig vond, als u vragen of opmerkingen heeft, laat ze dan hieronder.

logo

©2024 Scott Galloway