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
Op een werkproject heb ik HTMX gebruikt en misbruikt om een admin UI te bouwen. Als onderdeel hiervan gebruik ik de mooie SweetAlert2color Javascriptbibliotheek voor mijn bevestigingsdialogen. Het werkt geweldig, maar ik wilde ze ook gebruiken om mijn HTMX laadindicatoren te vervangen.
Dit bleek een Uitdaging te zijn, dus ik dacht ik documenteer het hier om jou dezelfde pijn te besparen.
Warning I'm a C# coder my Javascript is likely horrible.
[TOC]Dus HTMX is erg slim, het is hx-indicator
Normaal kunt u een laadindicator instellen voor uw HTMX-verzoeken. Normaal gesproken is dit een HTML element in uw pagina zoals
<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>
Als je het wilt gebruiken, versier je je HTMX-verzoek met hx-indicator="#loading-modal"
en zal de wijze waarop het verzoek wordt ingediend, worden aangegeven (zie hier voor details).
Nu HTMX doet wat slimme magie met behulp van een request
object het volgt intern
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
}
Het is dan ook een beetje een uitdaging om deze te vervangen. Hoe traceer je de verzoeken en laat dan de SweetAlert2 modal zien wanneer het verzoek in uitvoering is en verberg je het als het klaar is.
Dus ik stelde voor (niet omdat ik moest, want ik heb het nodig om:)) om de HTMX laadindicator te vervangen door een SweetAlert2 modal. Hoe dan ook, hier is de code die ik bedacht heb.
Je zou beginnen met het importeren van SweetAlert2 in je HTML (als script & style tags) / importeren voor webpack of soortgelijke (zie hun documenten hiervoor).
Na het installeren van npm kunt u het zo importeren in uw JS-bestand.
import Swal from 'sweetalert2';
Dan ziet mijn hoofdcode er zo uit:
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;
}
}
Je configureert dit (als je ESM gebruikt) in je main.js
bestand als dit
import { registerSweetAlertHxIndicator } from './hx-sweetalert-indicator.js';
registerSweetAlertHxIndicator();
Je zult zien dat ik gebruik maak van de getIndicatorSource
functie om het element te vinden dat het HTMX verzoek activeerde. Dit is belangrijk omdat we moeten weten welk element het verzoek activeerde, zodat we de modale kunnen sluiten als het klaar is. Dit is belangrijk omdat HTMX 'erfgoed' heeft, dus je moet de boom beklimmen om het element te vinden dat het verzoek activeerde.
function getIndicatorSource(el) {
return el.closest('[hx-indicator], [data-hx-indicator]');
}
Dan op elk HTMX verzoek (dus hx-get
of hx-post
) kunt u gebruik maken van de hx-indicator
attribuut om de SweetAlert2 modal te specificeren. Je hoeft niet eens de klasse zoals voorheen op te geven, alleen de bestaande parameter werkt.
Laten we doornemen hoe dit allemaal werkt:
registerSweetAlertHxIndicator()
Dit is het beginpunt. U kunt zien dat het haken in de htmx:configRequest
event. Dit wordt afgevuurd wanneer het HTMX op het punt staat een verzoek in te dienen.
Het krijgt dan het element dat de gebeurtenis activeerde in evt.detail.elt
en controleert of het een hx-indicator
attribuut.
Ten slotte toont het de SweetAlert2 modal using Swal.fire()
.
rt function registerSweetAlertHxIndicator() {
document.body.addEventListener('htmx:configRequest', function (evt) {
const trigger = evt.detail.elt;
const indicatorAttrSource = getIndicatorSource(trigger);
if (!indicatorAttrSource) return;
Als het doet, het krijgt het verzoek pad met behulp van getRequestPath(evt.detail)
en slaat het op in sessie-opslag.
Niw HTMX is een lastige bugger, het slaat het pad op verschillende plaatsen, afhankelijk van waar je bent in de levenscyclus. Dus in mijn code doe ik alles. met detail?.pathInfo?.path ?? detail?.path ?? '';
Het blijkt dat HTMX de verzoek pad in detail.path
en de response pad (voor document.body.addEventListener('htmx:afterRequest', maybeClose); document.body.addEventListener('htmx:responseError', maybeClose);
) in detail.PathInfo.responsePath
Dus we moeten ze allebei aanpakken.
We moeten ook omgaan met... GET
formulieren; als hun antwoord zal waarschijnlijk de URL-elementen die zijn doorgegeven als <input >
waarden zodat de respons url kan eindigen anders te zijn.
// 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;
}
OPMERKING: Dit is vooral het geval als u de HX-Push-Url
header om de URL van het verzoek te wijzigen die HTMX opslaat voor Geschiedenis.
HttpGet
formulieren zijn een beetje lastig dus we hebben een stuk code die zal detecteren als je hebt geklikt op een submit
knop in een formulier en voeg de query string parameters veroorzaakt door deze vervelende ingangen om te vergelijken met de response URL.
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"
}));
}
}
Deze tweede wordt hier behandeld:
document.body.addEventListener('sweetalert:close', closeSweetAlertLoader);
Oké, nu hebben we het pad, wat doen we ermee? Nou om bij te houden welk verzoek leidde tot de SweetAlert2 modale die we opslaan in sessionStorage
gebruik sessionStorage.setItem(SWEETALERT_PATH_KEY, path);
.
(Weer kun je dit complexer maken en ervoor zorgen dat je er maar één hebt als je dat nodig hebt.)
We tonen dan gewoon de SweetAlert2 modal using Swal.fire()
. Merk op dat we hier een hoop opties hebben.
Bij het openen van het controleert op een sessie opslagsleutel SWEETALERT_HISTORY_RESTORED_KEY
die wordt ingesteld wanneer de geschiedenis is hersteld. Als dat zo is, sluiten we de modale direct (het bespaart HTMX ons te verknoeien met het vreemde geschiedenisbeheer).
We vuren ook een gebeurtenis af. sweetalert:opened
die u kunt gebruiken om elke aangepaste logica die u nodig hebt te doen.
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;
}
});
Daarnaast hebben we een timeout ingesteld om zaken te behandelen waar het verzoek hangt. Dit is belangrijk omdat HTMX niet altijd sluit de modale als het verzoek mislukt (vooral als u gebruik hx-boost
). Dit is hier ingesteld. const SWEETALERT_TIMEOUT_MS = 10000; // Timeout for automatically closing the loader
dus we kunnen de modale sluiten als er iets mis gaat (het logt ook op de console).
Dus nu hebben we de modale open, we moeten sluiten wanneer het verzoek is afgerond. Om dit te doen noemen we de maybeClose
functie. Dit wordt aangeroepen als het verzoek is voltooid (met succes of met een fout).
Gebruik htmx:afterRequest
en htmx:responseError
gebeurtenissen. Deze gebeurtenissen vuren zodra HTMX klaar is met een verzoek (let op, deze zijn belangrijk, especialy voor HX-Boost
wat een beetje grappig kan zijn over wat voor gebeurtenissen het afvuurt.)
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);
}
}
U zult zien dat deze functie controleert of het pad in de sessieopslag hetzelfde is als het pad van het verzoek. Als dat zo is, sluit het de modale en verwijdert het pad uit de sessieopslag.
HTMX heeft een rare manier van omgaan met geschiedenis die de modale'stick' open zou kunnen laten op het maken van een back page. Dus voegen we nog een paar events toe om dit te vangen (meestal hebben we er maar één nodig maar riem & beugel).
//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');
});`
Je zult zien dat we ook de sessionStorage.setItem(SWEETALERT_HISTORY_RESTORED_KEY, 'true');
die we controleren in de didOpen
gebeurtenis:
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;
}
We doen het in het geval dat de modale niet al wasy open onmiddellijk op popstate
\ htmx:historyRestore
(vooral als je veel geschiedenis hebt). Dus we moeten het controleren in de didOpen
event (vandaar dat het in sessiesleutel is, soms kan dit herladen etc... dus we moeten ons daarvan bewust zijn).
Zo kun je SweetAlert2 dus gebruiken als een HTMX laadindicator. Het is een beetje een hack maar het werkt en het is een mooie manier om dezelfde bibliotheek te gebruiken voor zowel het laden van indicatoren en bevestiging dialogen.9