Utilisation de SweetAlert2 pour les indicateurs de chargement HTMX (hxM SK2indicateur)
Introduction
Dans le cadre d’un projet de travail, j’ai utilisé et maltraité HTMX pour construire une interface administrative. SweetAlert2 Bibliothèque de Javascript pour mes dialogues de confirmation. Il fonctionne bien, mais je voulais aussi les utiliser pour remplacer mes indicateurs de chargement HTMX.
Il s’est révélé qu’il s’agissait d’une épreuve, et j’ai donc pensé que je le documentais ici pour vous sauver la même douleur.
Warning I'm a C# coder my Javascript is likely horrible.
[TOC]Le problème
HTMX est donc très intelligent. hx-indicator normalement vous permet d'établir un indicateur de chargement pour vos demandes HTMX. Normalement, c'est un élément HTML dans votre page comme
<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>
Ensuite, lorsque vous voulez l'utiliser vous'd décorez votre demande HTMX avec hx-indicator="#loading-modal" et il indiquera le mode lorsque la demande est en cours (voir ici pour plus de détails).
Maintenant HTMX fait une certaine magie intelligente en utilisant request Objectif il suit à l'interne
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
}
Il est donc un peu difficile de les remplacer.
Une solution
Je me suis donc fixé à ( non parce que j’avais dû , parce que J’avais eu besoin de :)) pour remplacer l’indicateur de chargement HTMX par un Modèle SweetAlert Quoi qu’il en soit, c’est le code que j’ai trouvé.
Vous'd commencerez par importer SweetAlert2 dans votre HTML M SK2 en tant que script & balises de styleMSC4 / l’importer pour le webpack ou un autre document similaire.voir leurs dossiers pour ce
Après avoir installé npm, vous pouvez l’importer comme suit dans votre fichier JS.
import Swal from 'sweetalert2';
Puis mon code principal ressemble à ce qui suit:
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;
}
}
Vous configurez ce (si vous 'utilisez ESM) dans votre main.js fichier comme celui-ci
import { registerSweetAlertHxIndicator } from './hx-sweetalert-indicator.js';
registerSweetAlertHxIndicator();
Trouver nos éléments
Vous verrez que j’utilise le getIndicatorSource fonction pour trouver l'élément qui a déclenché la demande HTMX. C'est important car nous devons savoir quel élément a déclété la demande afin que nous puissions fermer le modal quand elle est terminée.
function getIndicatorSource(el) {
return el.closest('[hx-indicator], [data-hx-indicator]');
}
Ensuite, sur toute demande HTMX (so hx-get ou hx-post) vous pouvez utiliser le hx-indicator attribut pour spécifier le SweetAlert2 modal. Vous n’avez pas besoin de spécifier la classe comme avant , simplement les paramètres qui fonctionnent.
Let's passer en revue comment tout cela fonctionne:
Il s’agit d’une affaire qui ne peut être réglée qu’avec le consentement de l’employeur. registerSweetAlertHxIndicator()
Il s’agit d’un point de sortie. Vous pouvez le voir attacher à la htmx:configRequest événement. Cette fonction est activée lorsque HTMX est sur le point de faire une demande.
Il obtient ensuite l'élément qui a déclenché l'événement dans evt.detail.elt et vérifie s’il a un hx-indicator attribute.
Enfin, il montre le mode SweetAlert2 en utilisant Swal.fire().
rt function registerSweetAlertHxIndicator() {
document.body.addEventListener('htmx:configRequest', function (evt) {
const trigger = evt.detail.elt;
const indicatorAttrSource = getIndicatorSource(trigger);
if (!indicatorAttrSource) return;
Obtenir le chemin de la demande
Si elle le fait,, elle obtient la voie de demande en utilisant getRequestPath(evt.detail) et le stocke dans l’entreposage de session.
Niw HTMX est un bogueur délicat, il stocke le chemin en différents endroits selon où vous êtes dans le cycle de vie. Dans mon code, je fais tout ce qui suit detail?.pathInfo?.path ?? detail?.path ?? '';
Il s’avère que HTMX a stocké demande Voie d'entrée detail.path et le chemin de réponse (pour document.body.addEventListener('htmx:afterRequest', maybeClose); document.body.addEventListener('htmx:responseError', maybeClose);) en detail.PathInfo.responsePath Nous avons donc trop besoin de traiter les deux
Nous devons également GET les formulaires; car leur réponse inclura probablement les éléments URL transmis comme <input > valeurs pour que la réponse url soit différente.
// 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;
}
NOTE: C'est particulièrement le cas si vous utilisez HX-Push-Url header pour changer l'URL de la demande que HTMX stocke pour History.
Le formulaire
HttpGet Les formulaires sont un peu compliqués, nous avons donc une pièce de code qui détectera si vous avez submit et append les paramètres de la chaîne de demande causés par ces entrées nulles pour comparer à l’URL de réponse.
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"
}));
}
}
Cette seconde est traitée ici:
document.body.addEventListener('sweetalert:close', closeSweetAlertLoader);
Entreposage du chemin
Ok alors maintenant nous avons le chemin, ce que nous faisons avec lui? Bien pour suivre quelle demande a déclenché le Modèle SweetAlert 2 nous l'entreposons dans sessionStorage utilisation sessionStorage.setItem(SWEETALERT_PATH_KEY, path);.
( Encore une fois, vous pouvez le rendre plus complexe et s’assurer que vous n’en avez qu’un si vous en avez besoin.
Dévoilement du mode
Nous montrons ensuite simplement le mode SweetAlert2 en utilisant Swal.fire(). note que nous avons un certain nombre d’options ici
Au moment de l'ouverture, il vérifie la présence d'une clé de stockage de session. SWEETALERT_HISTORY_RESTORED_KEY qui est réglé lorsque l'histoire est restaurée. Si c'est le cas, nous cloisons immédiatement le modal ( cela sauve HTMX de nous en embarrasser avec elleMSC3 la gestion de l'historique étrangeM SK4
Nous lançons également un événement sweetalert:opened que vous pouvez utiliser pour faire toute logique personnalisée dont vous avez besoin.
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;
}
});
En outre, nous avons établi un délai pour traiter les cas où la demande est suspendue. C'est important car HTMX ne clout pas toujours le modal si la demande échoue ( particulièrement si vous utilisez hx-boost). Il s'agit ici const SWEETALERT_TIMEOUT_MS = 10000; // Timeout for automatically closing the loader pour que nous puissions fermer le mode si quelque chose va mal.
Clôture
Par conséquent, nous avons maintenant la modale ouverte, nous devons la fermer lorsque la demande est terminée . Pour ce faire, nous appelons le maybeClose fonction. Il est appelé lorsque la demande est terminée ( soit avec succès, soit avec une erreur
Utilisation htmx:afterRequest et htmx:responseError events. Ces événements disparaissent une fois que HTMX a terminé la demande. HX-Boost ce qui peut être un peu étrange quant aux événements qu’il déclenche.)
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);
}
}
Cette fonction vérifie si le chemin dans l'entreposage de session est le même que celui de la demande. Si c'est le cas, elle ferme le mode et enlève le chemin du stockage de session.
Il's Historique
HTMX dispose d’une façon fiddly de traiter l’histoire qui pourrait laisser le modal 'stuck' ouvert lors de la réalisation d’un page arrièreM SK2 Donc, nous ajoutons un couple d’événements pour capturer ce (pour la plupart du temps nous n’avons besoin qu’un seul, sauf de sa ceinture
//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');
});`
Vous verrez aussi que nous avons établi sessionStorage.setItem(SWEETALERT_HISTORY_RESTORED_KEY, 'true'); que nous vérifions dans le didOpen événement:
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;
}
Nous le faisons en cas de non-ouverture immédiate du mode popstate \ htmx:historyRestore (surtout si vous avez beaucoup d’histoire ). Nous devons donc le vérifier dans didOpen événement (c'est-à-dire qu'il est dans la clé de session , parfois il peut être rechargé, etc.
BONUS - Pour mon PagingTagHelper
Si vous utilisez mon Aide-mémoire à l'affichage Il y a une question avec le PageSize et en utilisant SweetAlert; c'est dû à ce qu'il ALSO colle le htmx:configRequestM SK1 event. Nous devons donc ajouter un petit code pour s'assurer qu'il ne conflicte pas avec le Modèle SweetAlert
(() => {
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;
})();
Pour y remédier, il faut ajouter une petite fonction:
/**
* 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;
}
Ce dernier reçoit simplement la demande et ajoute le pagesize Paramètre à lui. Importez ensuite cette fonction
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
Vous pouvez voir ici que nous détectons pageSize et lorsqu’il le trouve ( vous avez changé la taille de la page ) il utilise getPathWithPageSize fonction pour obtenir le nouveau chemin. Le pageSize Le code le remplacera ultérieurement dans la pipeline, mais cela lui permet de détecter correctement et de fermer comme prévu.
En conclusion
C'est ainsi qu'il est possible d'utiliser SweetAlert comme indicateur de chargement HTMX.