NOTE: Apart from
(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
Bei einem Arbeitsprojekt habe ich HTMX benutzt und missbraucht, um eine Admin-UI zu erstellen. Als Teil davon benutze ich die schöne SüßerAlert2 Javascript-Bibliothek für meine Bestätigungsdialoge......................................................................................................... Es funktioniert super, aber ich wollte sie auch nutzen, um meine HTMX Ladeindikatoren zu ersetzen.
Dies erwies sich als eine HERAUSFORDERUNG, also dachte ich, ich würde es hier dokumentieren, um Ihnen den gleichen Schmerz zu ersparen.
Warning I'm a C# coder my Javascript is likely horrible.
[TOC]Also ist HTMX sehr clever, es ist hx-indicator
Normalerweise können Sie eine Ladeanzeige für Ihre HTMX-Anfragen einstellen. Normalerweise ist dies ein HTML-Element in Ihrer Seite wie
<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>
Dann, wenn Sie es verwenden möchten, würden Sie Ihre HTMX-Anfrage mit dekorieren hx-indicator="#loading-modal"
und es wird zeigen, die Modal, wenn die Anfrage im Gange ist (siehe hier für Details).
Jetzt macht HTMX einige clevere Magie mit einem request
Objekt es trackt innerlich
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
}
Diese zu ersetzen, ist daher eine Herausforderung. Wie verfolgen Sie die Anfragen und zeigen dann die SweetAlert2 Modal, wenn die Anfrage im Gange ist, und verstecken Sie sie, wenn sie fertig ist.
Also habe ich (nicht, weil ich musste, weil ich brauchte :)) die HTMX Ladeanzeige durch einen SweetAlert2 Modal zu ersetzen. Wie auch immer, hier ist der Code, den ich erfunden habe.
Sie würden beginnen, indem Sie entweder SweetAlert2 in Ihrem HTML importieren (als Skript & Stil Tags) / importieren Sie es für Webpack oder ähnliches ((Das Parlament nimmt den Entwurf der legislativen Entschließung an.)
Nach der Installation von npm können Sie es so in Ihre JS-Datei importieren.
import Swal from 'sweetalert2';
Dann sieht mein Hauptcode so aus:
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;
}
}
Sie konfigurieren dies (wenn Sie ESM verwenden) in Ihrem main.js
Datei wie diese
import { registerSweetAlertHxIndicator } from './hx-sweetalert-indicator.js';
registerSweetAlertHxIndicator();
Du wirst sehen, ich benutze die getIndicatorSource
Funktion, um das Element zu finden, das die HTMX-Anfrage ausgelöst hat. Dies ist wichtig, da wir wissen müssen, welches Element die Anfrage ausgelöst hat, damit wir den Modal schließen können, wenn er fertig ist. Dies ist wichtig, da HTMX 'Vererbung' hat, so dass Sie den Baum klettern müssen, um das Element zu finden, das die Anfrage ausgelöst hat.
function getIndicatorSource(el) {
return el.closest('[hx-indicator], [data-hx-indicator]');
}
Dann auf jede HTMX-Anfrage (so hx-get
oder hx-post
) können Sie die hx-indicator
Attribut zur Angabe der SweetAlert2-Modalität. Sie müssen nicht einmal die Klasse wie vorher angeben, nur der Parameter vorhanden funktioniert.
Gehen wir durch, wie all das funktioniert:
registerSweetAlertHxIndicator()
Dies gilt als Out-Entry-Point. Sie können sehen, dass es Haken in die htmx:configRequest
.......................................................................................................................................... Dies wird abgefeuert, wenn HTMX eine Anfrage stellen wird.
Es erhält dann das Element, das das Ereignis ausgelöst in evt.detail.elt
und prüft, ob es eine hx-indicator
Attribut.
Schließlich zeigt es die SweetAlert2 Modal mit Swal.fire()
.
rt function registerSweetAlertHxIndicator() {
document.body.addEventListener('htmx:configRequest', function (evt) {
const trigger = evt.detail.elt;
const indicatorAttrSource = getIndicatorSource(trigger);
if (!indicatorAttrSource) return;
Wenn ja, erhält es den Request-Pfad mit getRequestPath(evt.detail)
und speichert es im Sitzungsspeicher.
Niw HTMX ist ein kniffliger Bugger, er speichert den Pfad an verschiedenen Orten, je nachdem, wo Sie sich im Lebenszyklus befinden. Also in meinem Code mache ich das ganze Above. mit detail?.pathInfo?.path ?? detail?.path ?? '';
Es stellt sich heraus, dass HTMX die Antrag Pfad in detail.path
und dem Antwortweg (für document.body.addEventListener('htmx:afterRequest', maybeClose); document.body.addEventListener('htmx:responseError', maybeClose);
) in der detail.PathInfo.responsePath
Also müssen wir auch beides regeln.
Wir müssen uns auch darum kümmern. GET
Form; da ihre Antwort wird wahrscheinlich die URL-Elemente in wie übergeben <input >
Werte, so dass die Antwort Url kann sich als anders.
// 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;
}
HINWEIS: Dies ist besonders der Fall, wenn Sie die HX-Push-Url
header, um die URL der Anfrage zu ändern, die HTMX für History speichert.
HttpGet
Formulare sind ein wenig schwierig, so haben wir ein Stück Code, die erkennen, wenn Sie geklickt haben eine submit
Schaltfläche innerhalb eines Formulars und fügen Sie die Abfrage String-Parameter durch diese lästigen Eingaben verursacht, um mit der Antwort-URL zu vergleichen.
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"
}));
}
}
Diese zweite wird hier behandelt:
document.body.addEventListener('sweetalert:close', closeSweetAlertLoader);
Ok, jetzt haben wir den Weg, was machen wir damit? Gut zu verfolgen, welche Anfrage ausgelöst die SweetAlert2 Modal speichern wir es in sessionStorage
Verwendung sessionStorage.setItem(SWEETALERT_PATH_KEY, path);
.
(Wieder können Sie diese komplexer machen und sicherstellen, dass Sie nur eine haben, wenn Sie brauchen.)
Dann zeigen wir einfach den SweetAlert2 Modal mit Swal.fire()
......................................................................................................... Beachten Sie, dass wir hier eine Reihe von Optionen haben.
Beim Öffnen prüft es nach einem Sitzungsspeicherschlüssel SWEETALERT_HISTORY_RESTORED_KEY
die gesetzt wird, wenn die Geschichte wiederhergestellt wird. Wenn ja, schließen wir den Modal sofort (es spart HTMX uns mit seinem ungeraden Historienmanagement durcheinander zu bringen).
Wir feuern auch ein Ereignis sweetalert:opened
die Sie verwenden können, um jede benutzerdefinierte Logik zu tun, die Sie brauchen.
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;
}
});
Zusätzlich setzen wir einen Timeout, um Fälle zu behandeln, in denen die Anfrage hängt. Dies ist wichtig, da HTMX nicht immer den Modal schließt, wenn die Anfrage fehlschlägt (besonders wenn Sie hx-boost
)== Einzelnachweise == Das ist hier festgelegt. const SWEETALERT_TIMEOUT_MS = 10000; // Timeout for automatically closing the loader
so können wir den Modal schließen, wenn etwas schief geht (es wird auch an der Konsole loggen).
Jetzt haben wir also die Modal-Open, wir müssen sie schließen, wenn die Anfrage fertig ist. Um dies zu tun, nennen wir die maybeClose
Funktion. Dies wird aufgerufen, wenn die Anfrage beendet ist (entweder erfolgreich oder mit einem Fehler).
Verwendung htmx:afterRequest
und htmx:responseError
Veranstaltungen. Diese Ereignisse feuern, sobald HTMX eine Anfrage abgeschlossen hat (Anm., diese sind wichtig, speziell für HX-Boost
was ein bisschen lustig über die Ereignisse sein kann, die es feuert.)
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);
}
}
Sie werden sehen, dass diese Funktion überprüft, ob der Pfad im Sitzungsspeicher der gleiche ist wie der Pfad der Anfrage. Wenn ja, schließt es den Modal und entfernt den Pfad aus dem Sitzungsspeicher.
HTMX hat eine fiddly Weise des Umgangs mit Geschichte, die das modale 'Stuck' offen lassen könnte, wenn sie eine Rückseite tut. So fügen wir ein paar weitere Ereignisse hinzu, um dies zu fangen (die meiste Zeit brauchen wir nur eins, aber Gürtel & Zahnspangen).
//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');
});`
Sie werden sehen, dass wir auch die sessionStorage.setItem(SWEETALERT_HISTORY_RESTORED_KEY, 'true');
die wir in der didOpen
Ereignis:
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;
}
Wir tun es in dem Fall, da der Modal nicht alwasy öffnet sofort auf popstate
\ htmx:historyRestore
(vor allem, wenn Sie eine Menge Geschichte haben). Also müssen wir es in der didOpen
Event (daher ist es in Sitzungsschlüssel, manchmal kann dies neu laden etc... also müssen wir uns dessen bewusst sein).
So können Sie also SweetAlert2 als HTMX-Ladeanzeige verwenden. Es ist ein bisschen ein Hack, aber es funktioniert und es ist eine schöne Möglichkeit, die gleiche Bibliothek für das Laden von Indikatoren und Bestätigungsdialoge zu verwenden.9