Back to "HTMX (und ein wenig Alpine.js) für ein SPA-ähnliches Erlebnis 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 (und ein wenig Alpine.js) für ein SPA-ähnliches Erlebnis in ASP.NET Core

Sunday, 15 September 2024

Einleitung

Auf dieser Seite verwende ich HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX HTMX ausgiebig, ist dies ein super einfacher Weg, um Ihre Website fühlen sich mehr responsive und glatter, ohne eine Menge von JavaScript zu schreiben.

HINWEIS: Ich bin noch nicht ganz zufrieden damit, HTMX hat einige Elemente, die diese knifflig machen

HTMX und ASP.NET Core

Mit HTMX mit ASP.NET Core ist ein bisschen ein Traum, Ich habe darüber geschrieben zuvor in Dieser Beitrag......................................................................................................... Es ist wirklich ziemlich einfach, Ihre Website 'HTMXify', und es ist eine gute Möglichkeit, Ihre Website fühlen sich mehr reagieren.

Normale Verknüpfungen

Als Teil meiner neuesten gehen rund mit HTMX Ich beschloss, so viele Links auf dieser Website verwenden HTMX wie möglich zu machen. Wenn Sie z.B. auf den 'Home' Button darüber klicken, wird nun HTMX verwendet, um den Seiteninhalt zu laden.

Ich erreiche dies, indem ich einfach einige Eigenschaften zum Link-Tag füge, wie folgt:

    <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 können Sie sehen, ich füge die hx-swap Attribut, das HTMX anweist, den Inhalt des Zielelements mit dem vom Server zurückgegebenen Inhalt zu tauschen.

HINWEIS: Verwenden Sie hx-pushurl NICHT, da dies dazu führt, dass der Browser die Seite auf dem Backbutton zweimal neu lädt.

Das hx-target Attribut sagt HTMX, wo der vom Server zurückgegebene Inhalt platziert werden soll. In diesem Fall ist es die #contentcontainer Element (wo der gesamte Seiteninhalt geladen ist).

Ich füge hinzu: hx-swap="show:window:top was macht das Fenster scrollen nach oben auf der Seite, wenn der Inhalt getauscht wird.

Das C# Teil davon ist ziemlich einfach, es dupliziert einfach meine normale Index-Methode, sondern lädt die Teilansicht. Der Code wird unten angezeigt. Auch hier wird HTMX.NET zur Erkennung der hx-request header und geben Sie die Teilansicht zurück. Es gibt auch einen Ausschnitt, um die normale Ansicht zurückzugeben, wenn die Anfrage keine HTMX-Anfrage ist.

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

Das Ergebnis ist, dass dieser Link (und andere, die die gleiche Technik verwenden) als 'AJAX' Anfragen geladen wird und der Inhalt ohne einen vollständigen Seitenneuladen ausgetauscht wird. Dies vermeidet den lästigen 'Lichtblitz', den man bekommt, wenn eine Seite aufgrund der Art und Weise, wie mein Thema wechselt, neu geladen wird.

So die Methode unten CAN gemacht werden, um zu arbeiten, aber es ist super knifflig und ein bisschen peinlich. HTMX arbeitet wirklich gerne mit dem attributbasierten Ansatz. Verwendung htmx.ajax ist eine Art von "wenn alles andere fehlschlägt" Methode und bedeutet, dass Sie mit Borwser Geschichte chaos müssen. Leider Browserverlauf mit pushState replaceState und popstate ist wirklich ein bisschen ein Schmerz im Hals (aber kann super stark für Randfälle sein).

Stattdessen, was ich jetzt tue, ist nur hinzufügen hx-boost zu dem Tag, der Blog-Inhalte enthält.

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

Dies scheint gut zu funktionieren und hx-boost kann auf Eltern-Methoden platziert werden und gilt für alle Kinder-Links. Es ist auch mehr Standard HTMX, ohne sich mit Geschichte etc..

In der seltenen Gelegenheit, dass Sie DON'T wollen, um HTMX für bestimmte Links können Sie nur hinzufügen hx-boost="false" zum Link.

Allerdings ein Problem mit diesem Ansatz war mein Blog Links wie dieser: HTMX für ein SPA-ähnliches Erlebnis die keine 'normalen' Verbindungen sind. Diese Links werden durch den Markdown Parser erzeugt; während ich eine MarkDig-Erweiterung schreibe, um die HTMX-Attribute zu den Links hinzuzufügen, habe ich mich entschieden, einen anderen Ansatz zu verwenden (weil ich bereits eine Tonne Inhalte habe, die ich nicht wiederholen möchte).

Stattdessen habe ich eine JavaScript-Funktion, die nach all diesen Arten von Link sucht, dann verwendet htmx.ajax um den Tausch zu machen. Das ist im Wesentlichen das, was HTMX ohnehin nur "manuell" macht.

Wie Sie sehen können, ist dies nur eine einfache Funktion, die für alle Links in der Suche div.prose Element, das mit einem / und fügt ihnen dann einen Event-Hörer hinzu. Wenn der Link geklickt wird, wird das Ereignis verhindert, die URL extrahiert und dann die htmx.ajax Funktion wird mit der URL und dem Zielelement zum Aktualisieren aufgerufen.

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

Zusätzlich, sobald der Swap gemacht wurde, schiebe ich dann die neue URL in den Browserverlauf und scrolle nach oben auf der Seite. Ich benutze diesen Ansatz an ein paar Stellen (wie die Suche), um eine schöne butterige glatte Erfahrung zu geben.

Zurück Knopf (Dies ist schwierig, um die Arbeit zu bekommen)

Die Rückwärtstaste in dieser Art von App kann problematisch sein; VIELE 'richtige' SPAs deaktivieren entweder die Rückwärtstaste oder setzen sich mit diesem fehlerhaften Verhalten. Allerdings wollte ich sicherstellen, dass der Rückwärtsknopf wie erwartet funktioniert. Hinweis: Um die Rück-Taste wie erwartet funktionieren zu lassen, müssen wir auch für die popState .......................................................................................................................................... Dieses Ereignis wird ausgelöst, wenn der Benutzer im Browserverlauf zurück oder vorwärts navigiert. Wenn dieses Ereignis ausgelöst wird, können wir den Inhalt für die aktuelle URL neu laden.

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 diesem Code verhindern wir das standardmäßige Browser-Popstate-Verhalten mit dem event.preventDefault() Ruf an. Wir extrahieren dann die aktuelle URL aus der window.location.href Eigenschaft und führen Sie eine HTMX AJAX Anfrage zum Laden des Inhalts für den aktuellen Zustand aus. Sobald der Inhalt geladen wurde, scrollen wir nach oben auf der Seite.

Wie bereits erwähnt, können Sie AUCH nicht verwenden hx-pushurl da dies dazu führt, dass der Browser die Seite auf dem Backbutton zweimal neu laden wird.

HTMX allgemeiner (und Alpine.js)

Wie Sie vielleicht gedacht haben, bin ich ein Fan von HTMX, kombiniert mit ASP.NET Core und einem Smattering von Alpine.js es ermöglicht devs, eine wirklich schöne Benutzererfahrung mit minimalem Aufwand zu schaffen. Das Wichtigste, was ich lieber als die "voll gebildeteren" Client-Side-Frameworks wie React / Angular etc... bevorzuge, ist, dass es mir immer noch eine volle serverseitige Rendering-Pipeline mit ASP.NET Core gibt, aber das 'Gefühl' eines SPA. Alpine.js ermöglicht einfache Client-Seite Interaktivität, eine sehr einfache Funktion, die ich kürzlich hinzugefügt wurde, meine Kategorien'versteckt', aber erweitern Sie mit einem Klick.

Die Verwendung von Alpine.js bedeutet, dass kein zusätzliches JavaScript erforderlich ist.

        <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 können Sie sehen, dass ich die x-data Attribut zum Erstellen eines neuen Alpine.js-Datenobjekts, in diesem Fall showCategories......................................................................................................... Dies ist ein Boolean, das gekippt wird, wenn die h4 element wird angeklickt. Das x-on:click Das Attribut wird verwendet, um das Klickereignis an die Toggle-Funktion zu binden.

In der Liste der Kategorien verwende ich dann die x-show Attribut zum Anzeigen oder Ausblenden der Liste der Kategorien basierend auf dem Wert von showCategories......................................................................................................... Ich benutze auch die x-transition Attribut, um einen schönen Dia-Down-Effekt hinzuzufügen, wenn die Kategorien angezeigt oder ausgeblendet werden. Das x-cloak Attribut wird verwendet, um zu verhindern, dass der 'Flicker' der Kategorien angezeigt wird, bevor das JavaScript ausgeführt wurde. Dafür habe ich eine kleine CSS-Klasse definiert:

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

Schlussfolgerung

So ist es, ein kurzer Artikel über die Verwendung von HTMX und ein Ort von Alpine.js, um Ihre Website fühlen sich mehr responsive und 'SPA-ähnlich'. Ich hoffe, Sie fanden dies nützlich, wenn Sie Fragen oder Kommentare haben, bitte zögern Sie nicht, sie unten zu lassen.

logo

©2024 Scott Galloway