Affichage du contenu Toast et Swapping avec HTMX (Et ASP.NET Core) (Français (French))

Affichage du contenu Toast et Swapping avec HTMX (Et 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

Présentation

HTMX est une excellente bibliothèque pour rendre vos applications web plus dynamiques et plus réactives. Dans ce billet, je vais vous montrer comment utiliser HTMX pour afficher une notification de toast et échanger du contenu sur la page.

L'une des « limites » de la norme HTMX est que vous n'avez généralement qu'un seul élément de contenu échangé de l'arrière. Cependant, cela peut être surmonté avec l'utilisation de HX-Trigger en-têtes et un peu de javascript.

AU TÔT

J'utilise ce simple système de notification des toasts depuis un moment. C'est une fonction simple qui prend un message, la durée, et le type (succès, erreur, avertissement) et affiche une notification toast sur la page.

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

Cela utilise un petit extrait HTML que je définit dans mon _Fichier Layout.cshtml (en utilisant mon préféré Tailwind CSS & DaisyUI). Notez le « bloc de conservation de classe » à la fin. C'est un petit truc pour s'assurer que les classes sont conservées dans la sortie HTML finale. C'est vraiment pour ma configuration de vent arrière que je regarde seulement 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>

Le vent de queue

Ici, je définit les fichiers à 'tree-shake' ainsi que définir quelques classes d'animation que le toast utilise.

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

Déclenchement

Le secret de faire cela tout le travail est d'utiliser le Fonction de l'en-tête HTMX Trigger.

Maintenant, 'normalement' vous le feriez définir ceci dans votre code html / rasoir actuel:

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

Ou vous pouvez le définir dans un événement post-requête. Donc vous faites quelque chose puis ça déclenche un nouvel événement.

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

C'est pratique si vous voulez simplement « faire quelque chose alors indiquez que c'est fait » mais dans mon cas, je veux échanger du contenu ET montrer un toast.

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

Dans mon cas, mon déclencheur s'appelle showToast et je passe dans un message et un drapeau de succès. Donc i mon JS J'ai défini un auditeur d'événement pour cet événement. C'est ce qui s'est passé. showToast fonction et passe dans le message et le drapeau de succès.

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

Alors pourquoi j'utilise ça? Eh bien, dans un projet de travail récent, je voulais prendre quelques mesures sur un utilisateur affiché dans une table. Je voulais montrer une notification toast et échanger le contenu de la ligne utilisateur avec le nouveau contenu.

userrow.png

Comme vous pouvez le voir, j'ai un BUNCH de boutons qui "fait des trucs" à l'utilisateur. Je voulais montrer une notification toast et échanger le contenu de la ligne utilisateur avec le nouveau contenu.

Donc, dans mon contrôle, j'ai un simple'switch' qui prend le nom de l'action, fait des choses puis retourne le nouveau résultat de requête.

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

        }
    }

Vous pouvez voir que j'ajoute aussi la HX-Trigger l'en-tête de la réponse. Il s'agit d'un objet JSON avec le showToast clé et une valeur d'un objet avec la toast et issuccess Les clés. Les toast clé est le message à montrer dans la notification de toast et le issuccess clé est un booléen indiquant si l'action a été réussie ou non.

Puis dans le _Row partiel J'ai les attributs HX (utilisant HTMX.Net) pour déclencher l'action.

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

Vous pouvez voir que j'utilise la cible closest tr pour échanger la ligne entière avec le nouveau contenu. C'est une façon simple de mettre à jour le contenu de la ligne sans avoir à faire une mise à jour complète de la page.

Vue partielle

C'est vraiment très simple et une excellente technique pour ASP.NET Core avec HTMX. Vous pouvez en option utiliser HTMX.Nets Request.IsHtmx` ici mais dans ce cas, je n'utilise ceci que depuis un callback HTMX.

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

Dans ce cas, la vue partielle _Row est une ligne de table simple avec les informations utilisateur et les boutons pour effectuer les actions.

Caractéristiques HTMX supplémentaires

J'utilise également quelques autres fonctionnalités HTMX pour améliorer l'expérience utilisateur.

Chargement

J'utilise aussi un simple loading modal indiquer que la demande est en cours. C'est une façon simple de montrer à l'utilisateur que quelque chose se passe en arrière-plan.

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

Confirmer

J'utilise aussi les hx-confirm attribut pour afficher une boîte de dialogue de confirmation avant que l'action ne soit exécutée. C'est une façon simple de s'assurer que l'utilisateur veut vraiment effectuer l'action. Cette méthode est utilisée SweetAlert2 pour afficher une boîte de dialogue de confirmation.

Maintenant, si vous ne le faites pas, HTMX fonctionne toujours, mais il utilise la boîte de dialogue standard "confirmer" du navigateur qui peut être un peu jarring pour l'utilisateur.

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

En conclusion

C'est une façon simple d'utiliser HTMX pour afficher une notification de toast et échanger du contenu sur la page. C'est une excellente façon de rendre vos applications web plus dynamiques et réactives.

logo

©2024 Scott Galloway