Uso de SweetAlert2 para indicadores HTMX (Español (Spanish))

Uso de SweetAlert2 para indicadores HTMX

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.

Monday, 21 April 2025

//

Less than a minute

Introducción

En un proyecto de trabajo he estado usando y abusando de HTMX para construir una interfaz de usuario de administración. Como parte de esto estoy usando la encantadora SweetAlert2 Biblioteca Javascript para mis diálogos de confirmación. Funciona muy bien, pero también quería usarlos para reemplazar mis indicadores de carga HTMX.

Esto resultó ser un reto así que pensé en documentarlo aquí para salvarte el mismo dolor.

Warning I'm a C# coder my Javascript is likely horrible.

[TOC]

El problema

Así que HTMX es muy inteligente, es hx-indicator normalmente le permite establecer un indicador de carga para sus solicitudes HTMX. Normalmente esto es un elemento HTML en su página como


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

Luego, cuando quieras usarlo, decorarías tu petición HTMX con hx-indicator="#loading-modal" y mostrará el modal cuando la solicitud esté en curso.

Ahora HTMX hace magia inteligente usando un request objeto se rastrea internamente

  function addRequestIndicatorClasses(elt) {
    let indicators = /** @type Element[] */ (findAttributeTargets(elt, 'hx-indicator'))
    if (indicators == null) {
      indicators = [elt]
    }
    forEach(indicators, function(ic) {
      const internalData = getInternalData(ic)
      internalData.requestCount = (internalData.requestCount || 0) + 1
      ic.classList.add.call(ic.classList, htmx.config.requestClass)
    })
    return indicators
  }

Por lo tanto, la sustitución de estos es un poco un reto. ¿Cómo hacer un seguimiento de las solicitudes y luego mostrar el modal SweetAlert2 cuando la solicitud está en curso y ocultarlo cuando se ha terminado.

Una solución

Así que me puse a trabajar (no porque tenía que hacerlo, porque NECESITÉ :)) para reemplazar el indicador de carga HTMX con un modal SweetAlert2. De todos modos, aquí está el código que se me ocurrió.

Empezaría importando SweetAlert2 en su HTML / importarlo para webpack o similar (ver sus documentos para esto).

import Swal from 'sweetalert2';

export function registerSweetAlertHxIndicator() {
    document.body.addEventListener('htmx:configRequest', function (evt) {
        const trigger = evt.detail.elt;
        const indicatorAttrSource = getIndicatorSource(trigger);
        if (!indicatorAttrSource) return;

        const path = getRequestPath(evt.detail);
        if (!path) return;

        evt.detail.indicator = null; // Disable HTMX's default class logic
        sessionStorage.setItem(SWEETALERT_PATH_KEY, path);

        Swal.fire({
            title: 'Loading...',
            allowOutsideClick: false,
            allowEscapeKey: false,
            showConfirmButton: false,
            theme: 'dark',
            didOpen: () => Swal.showLoading()
        });
    });

    function maybeClose(evt) {
        const activePath = sessionStorage.getItem(SWEETALERT_PATH_KEY);
        const path = getRequestPath(evt.detail);

        if (activePath && path && activePath === path) {
            Swal.close();
            sessionStorage.removeItem(SWEETALERT_PATH_KEY);
        }
    }

    document.body.addEventListener('htmx:afterRequest', maybeClose);
    document.body.addEventListener('htmx:responseError', maybeClose);
}

const SWEETALERT_PATH_KEY = 'swal-active-path';

function getIndicatorSource(el) {
    return el.closest('[hx-indicator], [data-hx-indicator]');
}


function getRequestPath(detail) {
    const basePath = detail?.pathInfo?.path ?? detail?.path ?? '';
    const responsePath = detail?.pathInfo?.responsePath ?? basePath;
    const elt = detail.elt;

    const isGet = (detail.verb ?? '').toUpperCase() === 'GET';
    const form = elt.closest('form');

    // ✅ If it's a GET form, append query params
    if (isGet && form) {
        const params = new URLSearchParams();

        for (const el of form.elements) {
            if (!el.name || el.disabled) continue;

            const type = el.type;
            if ((type === 'checkbox' || type === 'radio') && !el.checked) continue;
            if (type === 'submit') continue;

            params.append(el.name, el.value);
        }

        const queryString = params.toString();
        return queryString ? `${responsePath}?${queryString}` : responsePath;
    }

    // ✅ For all others, just return the response path
    return responsePath;
}
document.body.addEventListener('htmx:afterRequest', maybeClose);
document.body.addEventListener('htmx:responseError', maybeClose);


Configure esto (si está usando ESM) en su main.js archivo como este


import { registerSweetAlertHxIndicator } from './hx-sweetalert-indicator.js';
registerSweetAlertHxIndicator();

Encontrar nuestros elementos

Ya verás que uso el getIndicatorSource función para encontrar el elemento que desencadenó la petición HTMX. Esto es importante, ya que necesitamos saber qué elemento desencadenó la petición para que podamos cerrar el modal cuando esté terminado. Esto es importante ya que HTMX tiene 'herencia' por lo que es necesario escalar el árbol para encontrar el elemento que desencadenó la solicitud.

function getIndicatorSource(el) {
    return el.closest('[hx-indicator], [data-hx-indicator]');
}

A continuación, en cualquier solicitud HTMX (así que hx-get o hx-post) se puede utilizar el hx-indicator atributo para especificar el modal de SweetAlert2. Ni siquiera necesitas especificar la clase como antes, solo el parámetro existente funciona.

Repasemos cómo funciona todo esto:

Conectándolo con registerSweetAlertHxIndicator()

Esto actúa como punto de entrada. Puedes ver que se engancha en el htmx:configRequest evento. Esto se dispara cuando HTMX está a punto de hacer una solicitud.

Luego obtiene el elemento que desencadenó el evento en evt.detail.elt y comprueba si tiene un hx-indicator atributo.

Por último, muestra el modal SweetAlert2 utilizando Swal.fire().

rt function registerSweetAlertHxIndicator() {
    document.body.addEventListener('htmx:configRequest', function (evt) {
        const trigger = evt.detail.elt;
        const indicatorAttrSource = getIndicatorSource(trigger);
        if (!indicatorAttrSource) return;

Obtener la ruta de solicitud

Si lo hace, obtiene la ruta de solicitud usando getRequestPath(evt.detail) y lo almacena en el almacenamiento de sesión. Niw HTMX es un enculado complicado, almacena el camino en diferentes lugares dependiendo de dónde estés en el ciclo de vida. Así que en mi código hago todo lo demás. con detail?.pathInfo?.path ?? detail?.path ?? '';

Resulta que HTMX almacenaba el solicitud entrada detail.path y la ruta de respuesta (para document.body.addEventListener('htmx:afterRequest', maybeClose); document.body.addEventListener('htmx:responseError', maybeClose);) en detail.PathInfo.responsePath Así que necesitamos manejar ambas cosas.

También tenemos que manejar GET formularios; ya que su respuesta probablemente incluirá los elementos URL pasados como <input > valores para que la url de respuesta pueda terminar siendo diferente.

function getRequestPath(detail) {
    const basePath = detail?.pathInfo?.path ?? detail?.path ?? '';
    const responsePath = detail?.pathInfo?.responsePath ?? basePath;
    const elt = detail.elt;

    const isGet = (detail.verb ?? '').toUpperCase() === 'GET';
    const form = elt.closest('form');

    // ✅ If it's a GET form, append query params
    if (isGet && form) {
        const params = new URLSearchParams();

        for (const el of form.elements) {
            if (!el.name || el.disabled) continue;

            const type = el.type;
            if ((type === 'checkbox' || type === 'radio') && !el.checked) continue;
            if (type === 'submit') continue;

            params.append(el.name, el.value);
        }

        const queryString = params.toString();
        return queryString ? `${responsePath}?${queryString}` : responsePath;
    }

    // ✅ For all others, just return the response path
    return responsePath;
}

NOTA: Este es especialmente el caso si se utiliza la HX-Push-Url encabezado para cambiar la URL de la solicitud que HTMX almacena para History.

Yo uso este pequeño Response método de extensión para establecer el HX-Push-Url encabezado en mi aplicación ASP.NET Core.

public static class ResponseExtensions
{
    public static void PushUrl(this HttpResponse response, HttpRequest request)
    {
        response.Headers["HX-Push-Url"] = request.GetEncodedUrl();
    }
}

Almacenar el camino

Bien, ahora que tenemos el camino, ¿qué hacemos con él? Bueno, para hacer un seguimiento de qué petición se disparó el modal SweetAlert2 lo almacenamos en sessionStorage utilizando sessionStorage.setItem(SWEETALERT_PATH_KEY, path);.

(De nuevo puede hacer esto más complejo y asegurarse de que sólo tiene uno si lo necesita.)

Mostrar el modal

A continuación, simplemente mostrar el modal SweetAlert2 utilizando Swal.fire().

           Swal.fire({
    title: 'Loading...',
    allowOutsideClick: false,
    allowEscapeKey: false,
    showConfirmButton: false,
    theme: 'dark',
    didOpen: () => Swal.showLoading()
});

Cerrándolo.

Así que ahora tenemos el modal abierto, tenemos que cerrarlo cuando la solicitud esté terminada. Para hacer esto llamamos a la maybeClose función. Esto se llama cuando la petición está terminada (con éxito o con un error). Uso htmx:afterRequest y htmx:responseError Acontecimientos. Estos eventos se disparan una vez que HTMX ha terminado una solicitud (nota, estos son importantes, especial para HX-Boost que puede ser un poco divertido acerca de los eventos que dispara.)

    document.body.addEventListener('htmx:afterRequest', maybeClose);
    document.body.addEventListener('htmx:responseError', maybeClose);
    function maybeClose(evt) {
        const activePath = sessionStorage.getItem(SWEETALERT_PATH_KEY);
        const path = getRequestPath(evt.detail);

        if (activePath && path && activePath === path) {
            Swal.close();
            sessionStorage.removeItem(SWEETALERT_PATH_KEY);
        }
    }

Verá que esta función comprueba si la ruta en el almacenamiento de sesión es la misma que la ruta de la solicitud. Si lo es, cierra el modal y elimina la ruta del almacenamiento de sesión.

Labor futura

Esta es una implementación ingenua, pero Funciona. mayormente; en el futuro podría añadir algunas cosas:

  1. Sólo abrir 1 modal a la vez. (Esto emula el comportamiento HTMX).
  2. Tiempo de espera para los modos huérfanos (ahora mismo si cambias el lado del servidor url de respuesta de una manera inesperada, permanecerá abierto).

Conclusión

Así es como puedes usar SweetAlert2 como un indicador de carga HTMX. Es un poco de un truco, pero funciona y es una buena manera de utilizar la misma biblioteca para cargar indicadores y diálogos de confirmación.9

logo

©2024 Scott Galloway