Back to "Toast- en swappingcontent tonen met HTMX (En 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 Core HTMX

Toast- en swappingcontent tonen met HTMX (En ASP.NET Core)

Saturday, 12 April 2025

Inleiding

HTMX is een geweldige bibliotheek voor het maken van uw webapplicaties dynamischer en responsiever. In dit bericht laat ik je zien hoe je HTMX kunt gebruiken om een toastnotificatie te tonen en inhoud te ruilen op de pagina.

Een van de 'limitaties' in standaard HTMX is dat je meestal slechts één stuk inhoud van de achterkant hebt geruild. Dit kan echter worden overwonnen met het gebruik van HX-Trigger headers en een beetje javascript.

TOOSTEN

Ik gebruik al een tijdje dit simpele toast meldingssysteem. Het is een eenvoudige functie die een bericht, duur en type (succes, fout, waarschuwing) en toont een toast notificatie op de pagina.

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

Dit maakt gebruik van een beetje HTML knipsel ik definieer in mijn _Layout.cshtml-bestand (gebruik makend van mijn favoriete Tailwind CSS & DaisyUI). Let op het 'klasse-behoudsblok' aan het einde. Dit is een kleine truc om ervoor te zorgen dat de klassen worden bewaard in de uiteindelijke HTML-uitvoer. Dit is echt voor mijn achterwind setup als ik alleen kijk naar 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>

Achterwind

Hier definieer ik welke bestanden te 'tree-shake' uit evenals een aantal animatie klassen de toast gebruikt definiëren.

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")],
};

Getriggerd

Het geheim van dit alles te laten werken is het gebruik van de HTMX Trigger header functionaliteit.

Nu zou je 'normaal' definieer dit in uw werkelijke html / scheermes code:

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

Of u kunt het definiëren in een na aanvraag evenement. Dus als je iets doet, activeert het een nieuwe gebeurtenis.

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

Dit is handig als je gewoon iets wilt 'doen dan aangeven dat het gedaan is', maar in mijn geval wil ik wat content ruilen EN een toast laten zien.

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

In mijn geval wordt mijn trigger genoemd. showToast Ik geef een boodschap en een succesvlag door. Dus i mijn JS Ik heb een event luisteraar voor dit evenement gedefinieerd. Dit roept dan de showToast functie en gaat door in de boodschap en succes vlag.

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

Waarom gebruik ik dit dan? Nou in een recent werk project wilde ik wat actie ondernemen op een gebruiker weergegeven in een tabel. Ik wilde een toastmelding tonen en de inhoud van de gebruikersrij ruilen met de nieuwe inhoud.

userrow.png

Zoals je kunt zien heb ik een BUNCH van knoppen die 'dingen doen' aan de gebruiker. Ik wilde een toastmelding tonen en de inhoud van de gebruikersrij ruilen met de nieuwe inhoud.

Dus in mijn controller heb ik een simpele'switch' die de actienaam neemt, doet dingen dan het nieuwe request resultaat terug.

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

        }
    }

U kunt zien dat ik ook toevoegen van de HX-Trigger Header naar het antwoord. Dit is een JSON object met de showToast sleutel en een waarde van een object met de toast en issuccess Sleutels. De toast sleutel is het bericht te tonen in de toast notificatie en de issuccess sleutel is een booleaan die aangeeft of de actie succesvol was of niet.

Dan in de _Row gedeeltelijk heb ik de HX (met behulp van HTMX.Net) attributen om de actie te activeren.

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

Je kunt zien dat ik het doelwit gebruik. closest tr om de hele rij te ruilen met de nieuwe inhoud. Dit is een eenvoudige manier om de inhoud van de rij bij te werken zonder een volledige pagina te hoeven vernieuwen.

Gedeeltelijke weergave

Dit is echt heel eenvoudig en een geweldige techniek voor ASP.NET Core met HTMX. U kunt optioneel gebruik maken van HTMX.Nets Request.IsHtmx.Hier, maar in dit geval gebruik ik dit alleen van een 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);
    }

In dit geval de gedeeltelijke weergave _Row is een eenvoudige tabel rij met de gebruikersinformatie en de knoppen om de acties uit te voeren.

Extra HTMX-functies

Ik gebruik ook een paar meer HTMX functies om de gebruikerservaring beter te maken.

Laden

Ik gebruik ook een eenvoudige loading modal om aan te geven dat het verzoek loopt. Dit is een eenvoudige manier om de gebruiker te laten zien dat er iets gebeurt op de achtergrond.

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

Bevestigen

Ik gebruik ook de hx-confirm attribuut om een bevestigingsdialoog te tonen voordat de actie wordt uitgevoerd. Dit is een eenvoudige manier om ervoor te zorgen dat de gebruiker echt de actie wil uitvoeren. Dit maakt gebruik van SweetAlert2color om een bevestigingsdialoog te tonen.

Als je dit niet doet, werkt HTMX nog steeds, maar het gebruikt het standaard Browser 'bevestigen' dialoogvenster dat een beetje kan janken voor de gebruiker.

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

Conclusie

Dit is een eenvoudige manier om HTMX te gebruiken om een toast notificatie en swap content op de pagina te tonen. Dit is een geweldige manier om uw webapplicaties dynamischer en responsiever te maken.

logo

©2024 Scott Galloway