Visar toast och byta innehåll med HTMX (och ASP.NET Core) (Svenska (Swedish))

Visar toast och byta innehåll med HTMX (och ASP.NET Core)

Comments

NOTE: Apart from English (and even then it's questionable, I'm Scottish). These are machine translated in languages I don't read. If they're terrible please contact me.
You can see how this translation was done in this article.

Saturday, 12 April 2025

//

Less than a minute

Inledning

HTMX är ett bra bibliotek för att göra dina webbapplikationer mer dynamiska och lyhörda. I det här inlägget ska jag visa dig hur du använder HTMX för att visa en rostat bröd anmälan och byta innehåll på sidan.

En av "begränsningarna" i standard HTMX är att du vanligtvis bara har en enda del av innehållet bytt från baksidan. Detta kan dock övervinnas med hjälp av HX-Trigger Rubriker och lite javascript.

FÖRBÄTTRING

Jag har använt det här systemet ett tag nu. Det är en enkel funktion som tar ett meddelande, varaktighet, och typ (framgång, fel, varning) och visar en rostat meddelande på sidan.

Javascripten

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

Detta använder lite HTML-slippet jag definierar i min _Layout.cshtml fil (med min föredragna Tailwind CSS & DaisyUI). Lägg märke till "klassbevarande blocket" i slutet. Detta är ett litet trick för att se till att klasserna bevaras i den slutliga HTML-utmatningen. Detta är verkligen för min medvind installation som jag bara tittar på 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>

Vindrutetorkare

Här definierar jag vilka filer att "träd-skaka" från samt definiera några animation klasser 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")],
};

Utlöst

Hemligheten med att få allt detta att fungera är att använda HTMX Trigger-huvudfunktionalitet.

Nu "normalt" skulle du definiera detta i din faktiska HTML / rakhyvelkod:

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

Eller du kan definiera det i en efter förfrågan händelse. Så du gör något, sen utlöser det 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>

Detta är praktiskt om du bara vill "göra något sedan ange att det är gjort" men i mitt fall vill jag byta lite innehåll och visa en skål.

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

I mitt fall är min utlösare namngiven showToast Jag skickar in ett meddelande och en framgångsflagga. Så jag min JS Jag har definierat en händelse lyssnare för denna händelse. Detta kräver sedan in i showToast fungerar och passerar i meddelandet och framgång flaggan.

// 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 den här? I ett nytt arbetsprojekt ville jag vidta några åtgärder mot en användare som visas i en tabell. Jag ville visa en rostat bröd anmälan och byta innehållet i användarraden med det nya innehållet.

userrow.png

Som ni kan se har jag en BUNCH av knappar som "gör saker" till användaren. Jag ville visa en rostat bröd anmälan och byta innehållet i användarraden med det nya innehållet.

Så i min kontrollerade jag har en enkel "witch" som tar handlingens namn, gör saker sedan returnerar den nya begäran resultatet.

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

        }
    }

Du kan se Jag bifogar också HX-Trigger Huvudet på svaret. Detta är ett JSON objekt med showToast nyckel och ett värde på ett objekt med toast och issuccess Nycklarna. I detta sammanhang är det viktigt att se till att toast nyckeln är meddelandet att visa i rostat bröd anmälan och issuccess Nyckeln är en boolean som visar om åtgärden var framgångsrik eller inte.

Sedan i _Row partiell Jag har HX (med hjälp av HTMX.Net) attribut för att utlösa åtgärden.

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

Du kan se att jag använder målet closest tr Att byta hela raden med det nya innehållet. Detta är ett enkelt sätt att uppdatera innehållet i raden utan att behöva göra en uppdatering på hela sidan.

Partiell vy

Detta är verkligen mycket enkel och en bra teknik för ASP.NET Core med HTMX. Du kan välja att använda HTMX.Nets Begär.IsHtmx" här men i det här fallet använder jag bara detta från en HTMX callback.

    [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 detta fall den partiella åsikten _Row är en enkel tabellrad med användarinformationen och knapparna för att utföra åtgärderna.

Ytterligare HTMX-funktioner

Jag använder också ett par fler HTMX-funktioner för att göra användarupplevelsen bättre.

Laddar

Jag använder också en enkel loading modal ange att begäran pågår. Detta är ett enkelt sätt att visa 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äfta

Jag använder också hx-confirm attribut för att visa en bekräftelsedialogruta innan åtgärden utförs. Detta är ett enkelt sätt att se till att användaren verkligen vill utföra åtgärden. Detta använder SötaAlert2 för att visa en bekräftelsedialogruta.

Nu om du inte gör detta, HTMX fungerar fortfarande men det använder den vanliga webbläsaren "bekräfta" dialogrutan som kan vara lite jarring 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);
    });
});

Slutsatser

Detta är ett enkelt sätt att använda HTMX för att visa en rostat meddelande och byta innehåll på sidan. Detta är ett bra sätt att göra dina webbapplikationer mer dynamiska och lyhörda.

logo

©2024 Scott Galloway