Anzeigen von Toast- und Swapping-Inhalten mit HTMX (Und ASP.NET Core) (Deutsch (German))

Anzeigen von Toast- und Swapping-Inhalten mit HTMX (Und 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

Einleitung

HTMX ist eine großartige Bibliothek, um Ihre Web-Anwendungen dynamischer und responsiver zu machen. In diesem Beitrag zeige ich Ihnen, wie Sie HTMX verwenden, um eine Toast-Benachrichtigung anzuzeigen und Inhalte auf der Seite auszutauschen.

Eine der 'Begrenzungen' in Standard HTMX ist, dass Sie in der Regel nur ein einziges Stück Inhalt vom hinteren Ende getauscht haben. Allerdings kann dies mit der Verwendung von HX-Trigger Kopfzeilen und ein wenig Javascript.

TOAST

Ich benutze dieses einfache Toast-Benachrichtigungssystem seit einer Weile. Es ist eine einfache Funktion, die eine Nachricht, Dauer und Typ (Erfolg, Fehler, Warnung) und zeigt eine Toast-Benachrichtigung auf der Seite.

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

Dies benutzt ein kleines HTML-Snippet, das ich in meinem _Layout.cshtml Datei (mit meinem bevorzugten Tailwind CSS & DaisyUI). Beachten Sie am Ende den 'class conserving block'. Dies ist ein kleiner Trick, um sicherzustellen, dass die Klassen in der endgültigen HTML-Ausgabe erhalten bleiben. Das ist wirklich für mein Rückenwind-Setup, wie ich nur sehe 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>

Rückenwind

Hier definiere ich, welche Dateien von 'tree-shake' zu 'tree-shake' werden, sowie definiere einige Animationsklassen, die der Toast verwendet.

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

Ausgelöst

Das Geheimnis, dass dies alles funktioniert, ist die Verwendung der HTMX Trigger-Header-Funktionalität.

Nun, 'normalerweise' würdest du Definieren Sie dies in Ihrem aktuellen html / Rasierer-Code:

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

Oder Sie können es in einem Event nach Anfrage definieren. Man tut also etwas, dann löst es ein neues Ereignis aus.

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

Dies ist praktisch, wenn Sie nur'etwas tun wollen dann zeigen, dass es getan ist', aber in meinem Fall möchte ich einige Inhalte austauschen UND einen Toast zeigen.

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

In meinem Fall ist mein Auslöser benannt. showToast und ich übergebe eine Botschaft und eine Erfolgsflagge. Also i my JS Ich habe einen Event-Hörer für dieses Event definiert. Dies führt dann in die showToast Funktion und übergeben in der Botschaft und Erfolg Flagge.

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

Warum benutze ich das also? Nun, in einem aktuellen Arbeitsprojekt wollte ich etwas auf einen Benutzer, der in einer Tabelle angezeigt wird, einwirken. Ich wollte eine Toast-Benachrichtigung anzeigen und den Inhalt der Benutzerzeile mit dem neuen Inhalt austauschen.

userrow.png

Wie Sie sehen können, habe ich ein BUNCH von Tasten, die'tun Zeug', um den Benutzer. Ich wollte eine Toast-Benachrichtigung anzeigen und den Inhalt der Benutzerzeile mit dem neuen Inhalt austauschen.

Also in meinem kontrollierten habe ich einen einfachen 'Schalter', der den Aktionsnamen nimmt, gibt dann Sachen das neue Anfrageergebnis zurück.

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

        }
    }

Sie können sehen, dass ich auch die HX-Trigger header zur Antwort. Dies ist ein JSON-Objekt mit dem showToast Schlüssel und ein Wert eines Objekts mit dem toast und issuccess Schlüssel. Das toast Schlüssel ist die Nachricht, die in der Toast-Benachrichtigung und der issuccess Schlüssel ist ein Boolean, der anzeigt, ob die Aktion erfolgreich war oder nicht.

Dann in der _Row teilweise habe ich die HX (mit HTMX.Net) Attribute, um die Aktion auszulösen.

                     <!-- 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 siehst, ich benutze das Ziel. closest tr um die gesamte Zeile mit dem neuen Inhalt zu tauschen. Dies ist ein einfacher Weg, um den Inhalt der Zeile zu aktualisieren, ohne eine ganze Seite aktualisieren zu müssen.

Teilansicht

Das ist wirklich sehr einfach und eine tolle Technik für ASP.NET Core mit HTMX. Sie können optional HTMX.Net verwendens Anfrage.IsHtmx` hier, aber in diesem Fall benutze ich das nur von einem 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 diesem Fall die Teilansicht _Row ist eine einfache Tabellenzeile mit den Benutzerinformationen und den Schaltflächen, um die Aktionen durchzuführen.

Zusätzliche HTMX-Funktionen

Ich benutze auch ein paar weitere HTMX-Funktionen, um das Benutzererlebnis zu verbessern.

Laden

Ich benutze auch eine einfache loading modal die Angabe, dass der Antrag im Gange ist. Dies ist ein einfacher Weg, um dem Benutzer zu zeigen, dass etwas im Hintergrund passiert.

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

Bestätigen

Ich benutze auch die hx-confirm Attribut, um einen Bestätigungsdialog anzuzeigen, bevor die Aktion ausgeführt wird. Dies ist ein einfacher Weg, um sicherzustellen, dass der Benutzer wirklich die Aktion durchführen will. Dabei wird SüßerAlert2 um einen Bestätigungsdialog anzuzeigen.

Wenn Sie dies nun nicht tun, arbeitet HTMX noch, aber es verwendet den Standard-Browser-Dialog 'bestätigen', der für den Benutzer ein wenig störend sein kann.

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

Schlussfolgerung

Dies ist eine einfache Möglichkeit, HTMX zu verwenden, um eine Toast-Benachrichtigung anzuzeigen und Inhalte auf der Seite auszutauschen. Dies ist ein guter Weg, um Ihre Web-Anwendungen dynamischer und responsiver zu machen.

logo

©2024 Scott Galloway