Back to "Att visa bröd och byta innehåll med HTMX ( Och ASP.NET CoreM SK2"

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

Att visa bröd och byta innehåll med HTMX ( Och ASP.NET CoreM SK2

Saturday, 12 April 2025

Inleiding

HTMX är en fantastisk bibliotek för att göra din webbapplikation mer dynamisk och responsiv.

En av 'gränsningarna' i standard HTMX (iM SK3eMSC4 inte OOB-bytes) innebär att du vanligtvis bara har en enda bit innehåll som bytes från baksidan. HX-Trigger opskrifte och lite javascript.

NOTA: Du kan använda hx-swap-oob att byta ut två olika innehåll element, men detta är lite mer komplext och inte lika lätt att använda för att göra några JavaScripty saker.

Toast

I' har använt detta som en variant av det här enkla notificationssystemet i ett tag nu. Det är en enkel funktion som tar ett meddelande , loppet M SK3 och skriver ( framgång Mska5 fel Mske6 varning M Ska7 och visar en toast-avsikt på sidan

Den här versionen ' den senaste versionen ' har några fler blinkar runt ikoner

Javascript

// HTMX toast notification
// Simple HTMX toast handler for use with hx-on::after-request
window.showToast = (message, duration = 3000, type = 'info') => {
    const toast = document.getElementById('toast');
    const toastMessage = document.getElementById('toast-message');
    const toastText = document.getElementById('toast-text');
    const toastIcon = document.getElementById('toast-icon');

    // Reset classes
    toastMessage.className = 'alert shadow-lg gap-2 transition-all duration-300 ease-in-out cursor-pointer';
    toastIcon.className = 'bx text-2xl';

    // Add DaisyUI alert type
    const alertClass = `alert-${type}`;
    toastMessage.classList.add(alertClass);

    // Add icon class
    const iconMap = {
        success: 'bx-check-circle',
        error: 'bx-error-circle',
        warning: 'bx-error',
        info: 'bx-info-circle'
    };
    const iconClass = iconMap[type] || 'bx-bell';
    toastIcon.classList.add(iconClass);

    // Set the message
    toastText.textContent = message;

    // Add slide-in animation
    toastMessage.classList.add('animate-slide-in');
    toast.classList.remove('hidden');

    // Allow click to dismiss
    toastMessage.onclick = () => hideToast();

    // Auto-dismiss
    clearTimeout(window.toastTimeout);
    window.toastTimeout = setTimeout(() => hideToast(), duration);

    function hideToast() {
        toastMessage.classList.remove('animate-slide-in');
        toastMessage.classList.add('animate-fade-out');
        toastMessage.onclick = null;

        toastMessage.addEventListener('animationend', () => {
            toast.classList.add('hidden');
            toastMessage.classList.remove('animate-fade-out');
        }, { once: true });
    }
};

Det här använder en liten HTML snippet som jag definierar i min _Layout.cshtml-lêer ( med min prefererade Tailwind CSS ♫ & ♫ DaisyUI ♫). ♫ Lägg märke till ♫ class preserving block ♫ ' ♫ i slutet ♫ МSK6 ♫ Det här är ett litet trick för att säkerställa att klasserna är bevarade i den slutgiltiga HTML-outputen ♫ cshtml.

<div
        id="toast"
        class="toast toast-bottom fixed z-50 hidden w-full md:w-auto max-w-sm right-4 bottom-4"
>
    <div
            id="toast-message"
            class="alert shadow-lg gap-2 transition-all duration-300 ease-in-out cursor-pointer"
    >
        <i id="toast-icon" class="bx text-2xl"></i>
        <span id="toast-text">Notification message</span>
    </div>
</div>

<!-- class-preserving dummy block -->
<div class="hidden">
    <div class="alert alert-success alert-error alert-warning alert-info"></div>
    <i class="bx bx-check-circle bx-error-circle bx-error bx-info-circle bx-bell"></i>
    <div class="animate-slide-in animate-fade-out"></div>
</div>

Tailwind

Här definierar jag vilka plier som 'träd-shakeM SK2 kommer ifrån och definierar några animationsklassar som toast använder.

const defaultTheme = require("tailwindcss/defaultTheme");

module.exports = {
  content: ["./Views/**/*.cshtml", "./Areas/**/*.cshtml"],
  safelist: ["dark"],
  darkMode: "class",
  theme: {
    extend: {
      keyframes: {
        'slide-in': {
          '0%': { opacity: 0, transform: 'translateY(20px)' },
          '100%': { opacity: 1, transform: 'translateY(0)' },
        },
        'fade-out': {
          '0%': { opacity: 1 },
          '100%': { opacity: 0 },
        },
      },
      animation: {
        'slide-in': 'slide-in 0.3s ease-out',
        'fade-out': 'fade-out 0.5s ease-in forwards',
      },
  },
  plugins: [require("daisyui")],
};

Triggerad

Hemligheten för att göra allt detta bra är att använda HTMX Trigger-Headerfunktion.

Nu 'normalt ' skulle du definiera detta i din riktiga html / rasor kod:

<div hx-get="/clicked" hx-trigger="click[ctrlKey]">Control Click Me</div>

Eller så kan du definiera det i en efterfrågan händelse. Så när du gör något så triggar den en ny händelse

<button 
    hx-get="/api/do-something"
    hx-swap="none"
    hx-on::afterRequest="window.showToast('API call complete!', 3000, 'success')"
    class="btn btn-primary"
>
    Do Something
</button>

Det här är praktiskt om du bara vill "'", göra något, och sedan indikera det.

            Response.Headers.Append("HX-Trigger", JsonSerializer.Serialize(new
            {
                showToast = new
                {
                    toast = result.Message,
                    issuccess = result.Success
                }
            }));

I mitt fall är min aktivator showToast och jag skickar in ett meddelande och en framgångsflagga. Så i mitt JS har jag definierat en händelse lyssnare för den här händelsen showToast funktion och passerar i meddelandet och framgångsflaggen.

// Handles HX-Trigger: { "showToast": { "toast": "...", "issuccess": true } }
document.body.addEventListener("showToast", (event) => {
    const { toast, issuccess } = event.detail || {};
    const type = issuccess === false ? 'error' : 'success';
    showToast(toast || 'Done!', 3000, type);
});

ASP.NET

Så varför använder jag det här?

userrow.png

Som ni kan se har jag en BUCH av knappar som ' gör sakerM SK1 till användaren . Jag ville visa ett toast-avsikte och byta innehållet i användarens rad med det nya innehållet

Så i min kontrollerade har jag en enkel 'switch' som tar actionnamen , gör saker och sedan återverkar den nya begäranresultatet

    private async Task ApplyAction(string email, string useraction)
    {
        if (!string.IsNullOrWhiteSpace(useraction) &&
            Enum.TryParse<UserActionType>(useraction, true, out var parsedAction))
        {
            RequestResult result;

            switch (parsedAction)
            {
                case UserActionType.FlipRoles:
                    result = await userActionService.FlipRestaurantPermissions(email);
                    break;
                case UserActionType.UnflipRoles:
                    result = await userActionService.UnFlipRestaurantPermissions(email);
                    break;
                case UserActionType.Enable2FA:
                    result = await userActionService.ToggleMFA(email, true);
                    break;
                case UserActionType.Disable2FA:
                    result = await userActionService.ToggleMFA(email, false);
                    break;~~~~
                case UserActionType.RevokeTokens:
                    result = await userActionService.RevokeTokens(email);
                    break;
                case UserActionType.Lock:
                    result = await userActionService.Lock(email);
                    break;
                case UserActionType.Unlock:
                    result = await userActionService.Unlock(email);
                    break;
                case UserActionType.Nuke:
                    result = await userActionService.Nuke(email);
                    break;
                case UserActionType.Disable:
                    result = await userActionService.DisableUser(email);
                    break;
                case UserActionType.Enable:
                    result = await userActionService.EnableUser(email);
                    break;
                case UserActionType.ResetPassword:
                    result = await userActionService.ChangePassword(email);
                    break;
                case UserActionType.SendResetEmail:
                    result = await userActionService.SendResetEmail(email);
                    break;
                default:
                    result = new RequestResult(false, "Unknown action");
                    break;
                  
            }

            Response.Headers.Append("HX-Trigger", JsonSerializer.Serialize(new
            {
                showToast = new
                {
                    toast = result.Message,
                    issuccess = result.Success
                }
            }));

        }
    }

Ni kan se att jag också lägger till HX-Trigger header till svaret. Detta är ett JSON objekt med showToast nyckeln och en värde på ett objekt med toast och issuccess nyckeln. Den toast nyckeln är meddelandet som ska visas i toast-avsikten och issuccess nyckeln är en booleer som visar om handlingen var lyckad eller inte.

Sedan i _Row partiell Jag har HX (som använder HTMX.NetM SK2 egenskaper för att utlösa aktionen

                     <!-- Revoke Login Tokens -->
                            <button class="btn btn-xs btn-error border whitespace-normal text-wrap tooltip tooltip-left" data-tip="Revoke login tokens"
                                    hx-get hx-indicator="#loading-modal" hx-target="closest tr" hx-swap="outerHTML"
                                    hx-action="Row" hx-controller="Users"
                                    hx-route-email="@user.Email" hx-route-useraction="@UserActionType.RevokeTokens"
                                    hx-confirm="Are you sure you want to revoke the login tokens for this user?">
                                <i class="bx bx-power-off"></i> Revoke
                            </button>

Ni kan se att jag använder målet closest tr att byta hela raden med den nya innehållet. Det här är ett enkelt sätt att uppdatera innehållet i raden utan att behöva göra en komplett sidauppgradering.

Partiell syn

Det här är verkligen en väldigt enkel och bra teknik för ASP.NET Core med HTMX. Du kan optionellt använda HTMX.Nets begäran.IsHtmx` här men i det här fallet använder jag bara detta från en HTMX återkoppling

    [Route("row")]
 
    public async Task<IActionResult> Row(string email, string? useraction = null)
    {

        if(!string.IsNullOrEmpty(useraction))
          await ApplyAction(email, useraction);

        var userRow = await userViewService.GetSingleUserViewModel(email);
        return PartialView("_Row", userRow);
    }

I det här fallet _Row är en enkel tabell rad med användarinformation och knapparna för att utföra aktionerna.

Aditionella HTMX-feature

Jag använder också några fler HTMX-features för att göra användarupplevelsen bättre.

Loading

Jag använder också en enkel loading modal för att indikera att begäran är på gång. Det här är ett enkelt sätt att visa till användaren att något händer i bakgrunden

<div id="loading-modal" class="modal htmx-indicator">
    <div
        class="modal-box flex flex-col items-center justify-center bg-base-200 border border-base-300 shadow-xl rounded-xl text-base-content dark text-center ">
        <div class="flex flex-col items-center space-y-4">
            <h2 class="text-lg font-semibold tracking-wide">Loading...</h2>
            <span class="loading loading-dots loading-xl text-4xl text-stone-200"></span>
        </div>
    </div>
</div>

Bekräftelse

Jag använder också hx-confirm attribut för att visa en bekräftande dialog innan handlingen är utförd. Det här är ett enkelt sätt att säkerställa att användaren verkligen vill utföra handlingen SweetAlert2 för att visa ett bekräftande dialog.

Nu om du DON'T gör detta, HTMX fungerar fortfarande men den använder standardbrowsern i dialogen | ' | confirm |' , som kan vara lite stökig för användaren

// HTMX confirm with SweetAlert2
window.addEventListener('htmx:confirm', (e) => {
    const message = e.detail.question;
    if (!message) return;

    e.preventDefault();

    Swal.fire({
        title: 'Please confirm',
        text: message,
        icon: 'warning',
        showCancelButton: true,
        confirmButtonText: 'Yes',
        cancelButtonText: 'Cancel',
        theme: 'dark',
    }).then(({ isConfirmed }) => {
        if (isConfirmed) e.detail.issueRequest(true);
    });
});

I slutsats

Det här är ett enkelt sätt att använda HTMX för att visa ett toast-avsikte och byta innehåll på sidan.

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.