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
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]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 (ver aquí para más detalles).
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.
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ó.
Comenzarías importando SweetAlert2 en tu HTML (como script y etiquetas de estilo) / importarlo para webpack o similar (ver sus documentos para esto).
Después de la instalación de npm puede importarlo así en su archivo JS.
import Swal from 'sweetalert2';
Entonces mi código principal se ve así:
import Swal from 'sweetalert2';
const SWEETALERT_PATH_KEY = 'swal-active-path'; // Stores the path of the current SweetAlert
const SWEETALERT_HISTORY_RESTORED_KEY = 'swal-just-restored'; // Flag for navigation from browser history
const SWEETALERT_TIMEOUT_MS = 10000; // Timeout for automatically closing the loader
let swalTimeoutHandle = null;
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;
const currentPath = sessionStorage.getItem(SWEETALERT_PATH_KEY);
// Show SweetAlert only if the current request path differs from the previous one
if (currentPath !== path) {
closeSweetAlertLoader();
sessionStorage.setItem(SWEETALERT_PATH_KEY, path);
evt.detail.indicator = null; // Disable HTMX's default indicator behavior
Swal.fire({
title: 'Loading...',
allowOutsideClick: false,
allowEscapeKey: false,
showConfirmButton: false,
theme: 'dark',
didOpen: () => {
// Cancel immediately if restored from browser history
if (sessionStorage.getItem(SWEETALERT_HISTORY_RESTORED_KEY) === 'true') {
sessionStorage.removeItem(SWEETALERT_HISTORY_RESTORED_KEY);
Swal.close();
return;
}
Swal.showLoading();
document.dispatchEvent(new CustomEvent('sweetalert:opened'));
// Set timeout to auto-close if something hangs
clearTimeout(swalTimeoutHandle);
swalTimeoutHandle = setTimeout(() => {
if (Swal.isVisible()) {
console.warn('SweetAlert loading modal closed after timeout.');
closeSweetAlertLoader();
}
}, SWEETALERT_TIMEOUT_MS);
},
didClose: () => {
document.dispatchEvent(new CustomEvent('sweetalert:closed'));
sessionStorage.removeItem(SWEETALERT_PATH_KEY);
clearTimeout(swalTimeoutHandle);
swalTimeoutHandle = null;
}
});
} else {
// Suppress HTMX indicator if the path is already being handled
evt.detail.indicator = null;
}
});
//Add events to close the loader
document.body.addEventListener('htmx:afterRequest', maybeClose);
document.body.addEventListener('htmx:responseError', maybeClose);
document.body.addEventListener('sweetalert:close', closeSweetAlertLoader);
// Set a flag so we can suppress the loader immediately if navigating via browser history
document.body.addEventListener('htmx:historyRestore', () => {
sessionStorage.setItem(SWEETALERT_HISTORY_RESTORED_KEY, 'true');
});
window.addEventListener('popstate', () => {
sessionStorage.setItem(SWEETALERT_HISTORY_RESTORED_KEY, 'true');
});
}
// Returns the closest element with an indicator attribute
function getIndicatorSource(el) {
return el.closest('[hx-indicator], [data-hx-indicator]');
}
// Determines the request path, including query string if appropriate
function getRequestPath(detail) {
const responsePath =
typeof detail?.pathInfo?.responsePath === 'string'
? detail.pathInfo.responsePath
: (typeof detail?.pathInfo?.path === 'string'
? detail.pathInfo.path
: (typeof detail?.path === 'string' ? detail.path : '')
);
const elt = detail.elt;
// If not a form and has an hx-indicator, use the raw path
if (elt.hasAttribute("hx-indicator") && elt.tagName !== "FORM") {
return responsePath;
}
const isGet = (detail.verb ?? '').toUpperCase() === 'GET';
const form = elt.closest('form');
// Append query params for GET form submissions
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;
}
return responsePath;
}
// Closes the SweetAlert loader if the path matches
function maybeClose(evt) {
const activePath = sessionStorage.getItem(SWEETALERT_PATH_KEY);
const path = getRequestPath(evt.detail);
if (activePath && path && activePath === path) {
closeSweetAlertLoader();
}
}
// Close and clean up SweetAlert loader state
function closeSweetAlertLoader() {
if (Swal.getPopup()) {
Swal.close();
document.dispatchEvent(new CustomEvent('sweetalert:closed'));
sessionStorage.removeItem(SWEETALERT_PATH_KEY);
clearTimeout(swalTimeoutHandle);
swalTimeoutHandle = null;
}
}
Configure esto (si está usando ESM) en su main.js
archivo como este
import { registerSweetAlertHxIndicator } from './hx-sweetalert-indicator.js';
registerSweetAlertHxIndicator();
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:
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;
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.
// Returns the closest element with an indicator attribute
function getIndicatorSource(el) {
return el.closest('[hx-indicator], [data-hx-indicator]');
}
// Determines the request path, including query string if appropriate
function getRequestPath(detail) {
const responsePath =
typeof detail?.pathInfo?.responsePath === 'string'
? detail.pathInfo.responsePath
: (typeof detail?.pathInfo?.path === 'string'
? detail.pathInfo.path
: (typeof detail?.path === 'string' ? detail.path : '')
);
const elt = detail.elt;
// If not a form and has an hx-indicator, use the raw path
if (elt.hasAttribute("hx-indicator") && elt.tagName !== "FORM") {
return responsePath;
}
const isGet = (detail.verb ?? '').toUpperCase() === 'GET';
const form = elt.closest('form');
// Append query params for GET form submissions
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;
}
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.
HttpGet
los formularios son un poco complicados por lo que tenemos un pedazo de código que detectará si usted ha hecho clic en un submit
botón dentro de un formulario y añadir los parámetros de la cadena de consulta causados por esas entradas molestas para comparar con la URL de respuesta.
const isGet = (detail.verb ?? '').toUpperCase() === 'GET';
const form = elt.closest('form');
// Append query params for GET form submissions
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;
}
return responsePath;
```
This is important as HTMX will use the response URL to determine if the request is the same as the previous one. So we need to ensure we have the same URL in both places.
### Extensions
I use this little `Response` extension method to set the `HX-Push-Url` header in my ASP.NET Core app. I also added a second extension which will immediately close the modal (useful if you mess with the request and need to close it immediately).
```csharp
public static class ResponseExtensions
{
public static void PushUrl(this HttpResponse response, HttpRequest request)
{
response.Headers["HX-Push-Url"] = request.GetEncodedUrl();
}
}
public static void CloseSweetAlert(this HttpResponse response)
{
response.Headers.Append("HX-Trigger" , JsonSerializer.Serialize(new
{
sweetalert = "closed"
}));
}
}
Este segundo se maneja aquí:
document.body.addEventListener('sweetalert:close', closeSweetAlertLoader);
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.)
A continuación, simplemente mostrar el modal SweetAlert2 utilizando Swal.fire()
. nota que tenemos un montón de opciones aquí.
Al abrirlo comprueba una clave de almacenamiento de sesión SWEETALERT_HISTORY_RESTORED_KEY
que se establece cuando la historia es restaurada. Si lo es, cerramos el modal de inmediato (salva a HTMX estropeándonos con su extraña gestión de la historia).
También disparamos un evento. sweetalert:opened
que puedes usar para hacer cualquier lógica personalizada que necesites.
Swal.fire({
title: 'Loading...',
allowOutsideClick: false,
allowEscapeKey: false,
showConfirmButton: false,
theme: 'dark',
didOpen: () => {
// Cancel immediately if restored from browser history
if (sessionStorage.getItem(SWEETALERT_HISTORY_RESTORED_KEY) === 'true') {
sessionStorage.removeItem(SWEETALERT_HISTORY_RESTORED_KEY);
Swal.close();
return;
}
Swal.showLoading();
document.dispatchEvent(new CustomEvent('sweetalert:opened'));
// Set timeout to auto-close if something hangs
clearTimeout(swalTimeoutHandle);
swalTimeoutHandle = setTimeout(() => {
if (Swal.isVisible()) {
console.warn('SweetAlert loading modal closed after timeout.');
closeSweetAlertLoader();
}
}, SWEETALERT_TIMEOUT_MS);
},
didClose: () => {
document.dispatchEvent(new CustomEvent('sweetalert:closed'));
sessionStorage.removeItem(SWEETALERT_PATH_KEY);
clearTimeout(swalTimeoutHandle);
swalTimeoutHandle = null;
}
});
Además, establecemos un tiempo de espera para manejar los casos en los que la solicitud se cuelga. Esto es importante ya que HTMX no siempre cierra el modal si la solicitud falla (especialmente si utiliza hx-boost
). Esto está aquí. const SWEETALERT_TIMEOUT_MS = 10000; // Timeout for automatically closing the loader
así que podemos cerrar el modal si algo sale mal (también se registrará en la consola).
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.
HTMX tiene una forma fiddly de manejar la historia que podría dejar el modal 'atascado' abierto en hacer una página trasera. Así que agregamos un par de eventos más para atrapar esto (la mayoría de las veces sólo necesitamos uno, pero correa y aparatos ortopédicos).
//Add events to close the loader
document.body.addEventListener('htmx:afterRequest', maybeClose);
document.body.addEventListener('htmx:responseError', maybeClose);
document.body.addEventListener('sweetalert:close', closeSweetAlertLoader);
// Set a flag so we can suppress the loader immediately if navigating via browser history
document.body.addEventListener('htmx:historyRestore', () => {
sessionStorage.setItem(SWEETALERT_HISTORY_RESTORED_KEY, 'true');
});
window.addEventListener('popstate', () => {
sessionStorage.setItem(SWEETALERT_HISTORY_RESTORED_KEY, 'true');
});`
Verá que también establecemos el sessionStorage.setItem(SWEETALERT_HISTORY_RESTORED_KEY, 'true');
que comprobamos en el didOpen
evento:
didOpen: () => {
// Cancel immediately if restored from browser history
if (sessionStorage.getItem(SWEETALERT_HISTORY_RESTORED_KEY) === 'true') {
sessionStorage.removeItem(SWEETALERT_HISTORY_RESTORED_KEY);
Swal.close();
return;
}
Lo hacemos en caso de que el modal no abra inmediatamente. popstate
\ htmx:historyRestore
(especialmente si tienes mucha historia). Así que tenemos que comprobarlo en el didOpen
evento (de ahí que esté en clave de sesión, a veces esto puede recargar, etc... así que tenemos que ser conscientes de eso).
Si estás usando mi PagingTagHelper Hay un problema con el PageSize
y el uso de SweetAlert; esto se debe a que TAMBIÉN enganchar el
htmx:configRequest` event. Así que tenemos que añadir un poco de código para asegurarnos de que no entra en conflicto con el modal SweetAlert2.
(() => {
if (window.__pageSizeListenerAdded) return;
document.addEventListener('htmx:configRequest', event => {
const { elt } = event.detail;
if (elt?.matches('[name="pageSize"]')) {
const params = new URLSearchParams(window.location.search);
params.set('pageSize', elt.value);
const paramObj = Object.fromEntries(params.entries());
event.detail.parameters = paramObj;
const pageSizeEvent = new CustomEvent('pagesize:updated', {
detail: {
params: paramObj,
elt,
},
});
document.dispatchEvent(pageSizeEvent);
}
});
window.__pageSizeListenerAdded = true;
})();
Para solucionar esto es necesario añadir una pequeña función:
/**
* Appends the current pageSize from the browser's URL to the HTMX request path,
* modifying event.detail.path directly.
*
* @param {CustomEvent['detail']} detail - The HTMX event.detail object
*/
export function getPathWithPageSize(detail) {
const originalPath = detail.path;
if (!originalPath) return null;
const selectedValue = detail.elt?.value ?? '';
const url = new URL(originalPath, window.location.origin);
// Use the current location's query string, update pageSize
const locationParams = new URLSearchParams(window.location.search);
locationParams.set('pageSize', selectedValue);
url.search = locationParams.toString();
return url.pathname + url.search;
}
Esto sólo recibe la petición y añade el pagesize
parámetro a él. A continuación, importar esta función
import { getPathWithPageSize } from './pagesize-sweetalert'
export function registerSweetAlertHxIndicator() {
document.body.addEventListener('htmx:configRequest', function (evt) {
const trigger = evt.detail.elt;
const indicatorAttrSource = getIndicatorSource(trigger);
if (!indicatorAttrSource) return;
// ✅ If this is a pageSize-triggered request, use our custom path
let path;
if (evt.detail.headers?.['HX-Trigger-Name'] === 'pageSize') {
path = getPathWithPageSize(evt.detail);
console.debug('[SweetAlert] Using custom path with updated pageSize:', path);
} else {
path = getRequestPath(evt.detail);
}
...rest of code
Se puede ver aquí que detectamos el pageSize
activador y cuando lo encuentra (por lo que cambió el tamaño de las páginas) utiliza el getPathWithPageSize
función para obtener el nuevo camino. Los pageSize
código reemplazará esto más adelante en la tubería, pero esto le permite detectar correctamente por lo que se cierra como se esperaba.
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