SweetAlert2:n käyttö HTMX-laajennusmittareihin (hx-indikaattori) (Suomi (Finnish))

SweetAlert2:n käyttö HTMX-laajennusmittareihin (hx-indikaattori)

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

Johdanto

Työprojektissa olen käyttänyt ja käyttänyt väärin HTMX:ää admin-UI:n rakentamiseen. Osana tätä käytän ihanaa SweetAlert2 Javascript-kirjasto Vahvistusikkunoilleni...................................................................................................................................... Se toimii hyvin, mutta halusin käyttää niitä myös HTMX-latausmittareideni korvaamiseen.

Tämä osoittautui haasteeksi, joten ajattelin dokumentoida sen täällä säästääkseni sinulta saman tuskan.

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

[TOC]

Ongelma

Joten HTMX on erittäin fiksu, se on hx-indicator Normaalisti voit asettaa latausmittarin HTMX-pyyntöihisi. Tavallisesti tämä on HTML-elementti sivullasi, kuten


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

Kun haluat käyttää sitä, koristelet HTMX-pyyntösi hx-indicator="#loading-modal" ja se näyttää liikennemuodon, kun pyyntö on käynnissä (katso tarkemmat tiedot täältä).

Nyt HTMX tekee nokkelaa taikaa käyttäen request esine se seuraa sisäisesti

  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
  }

Siksi näiden korvaaminen on hieman haasteellista. Miten voit seurata pyyntöjä ja sitten näyttää SweetAlert2-moodin, kun pyyntö on käynnissä, ja piilottaa sen, kun se on valmis.

Ratkaisu

Niinpä ryhdyin (ei siksi, että minun oli pakko, koska tarvitsin :) korvaamaan HTMX-latausmittarin SweetAlert2-modaalilla. Tässä on koodi, jonka keksin.

Aloita tuomalla SweetAlert2 HTML:ssäsi (skripti- ja tyylitageina) tai tuomalla se webpackille tai vastaavalle (katso tästä heidän dokumenttejaan).

Kun npm on asennettu, voit tuoda sen näin JS-tiedostoosi.

import Swal from 'sweetalert2';

Sitten pääkoodini näyttää tältä:

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

Määritä tämä (jos käytät EVM:ää) main.js tämmöinen tiedosto


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

Elementtien löytäminen

Huomaat, että käytän getIndicatorSource Funktio HTMX-pyynnön käynnistäneen elementin löytämiseksi. Tämä on tärkeää, koska meidän on tiedettävä, mikä osa käynnisti pyynnön, jotta voimme sulkea liikennemuodon, kun se on valmis. Tämä on tärkeää, koska HTMX:llä on "perimys", joten sinun täytyy kiivetä puuhun löytääksesi elementti, joka laukaisi pyynnön.

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

Sitten mistä tahansa HTMX-pyynnöstä (siis hx-get tai hx-post) voit käyttää hx-indicator Ominaisuus, jolla määritetään SweetAlert2-modaali. Luokkaa ei tarvitse edes määritellä kuten aiemmin, vain olemassa olevat parametrit.

Käydään läpi, miten tämä kaikki toimii:

Kytketään se yhteen registerSweetAlertHxIndicator()

Tämä toimii ulostulopaikkana. Sen näkee koukuttavan htmx:configRequest tapahtuma. Tämä irtisanotaan, kun HTMX on aikeissa esittää pyynnön.

Se saa sitten elementin, joka laukaisi tapahtuman evt.detail.elt ja tarkistaa, onko sillä hx-indicator Ominaisuus.

Lopuksi siinä esitetään SweetAlert2-modaali, jossa käytetään Swal.fire().

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

Pyyntöpolun saaminen

Jos näin käy, se saa pyyntöpolun käyttämällä getRequestPath(evt.detail) ja tallentaa sen istuntotallenteeseen. Now HTMX on hankala otus, se tallentaa polun eri paikkoihin riippuen siitä, missä elinkaaressa olet. Joten minun koodissani teen kaiken abortin. yy) kanssa, kun detail?.pathInfo?.path ?? detail?.path ?? '';

Kävi ilmi, että HTMX tallensi pyyntö polku sisään detail.path ja vastauspolku ( document.body.addEventListener('htmx:afterRequest', maybeClose); document.body.addEventListener('htmx:responseError', maybeClose);) detail.PathInfo.responsePath meidän täytyy siis hoitaa molemmat.

Meidän on myös hoidettava GET lomakkeet; koska niiden vastaus todennäköisesti sisältää URL-elementit läpäissyt <input > Arvot, jotta vastaus voi päätyä erilaiseksi.

// 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;
}

HUOMAUTUS: Näin on erityisesti, jos käytät HX-Push-Url Otsikko muuttaa pyynnön URL-osoitteen, joka HTMX tallentaa historiaan.

Lomake

HttpGet Lomakkeet ovat hieman hankalia, joten meillä on koodi, joka havaitsee, jos olet klikannut submit Napauta lomakkeen sisällä ja liitä kyselyjonoparametrit, jotka johtuvat näistä pesivistä syötteistä, vastausosoitteeseen.

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"
        }));

    }
}

Toinen käsitellään tässä:

    document.body.addEventListener('sweetalert:close', closeSweetAlertLoader);

Polun säilyttäminen

Okei, nyt meillä on polku, mitä teemme sille? Pitääksemme kirjaa siitä, minkä pyynnön SweetAlert2-modaali laukaisi, tallennamme sen sessionStorage Käytössä sessionStorage.setItem(SWEETALERT_PATH_KEY, path);.

(Taas voit tehdä tästä monimutkaisemman ja varmistaa, että sinulla on vain yksi, jos tarvitset.)

Liikennemuodon näyttäminen

Sitten näytämme vain SweetAlert2-modaalin Swal.fire()...................................................................................................................................... Huomaa, että meillä on tässä joukko vaihtoehtoja.

Avattaessa se tarkistaa istuntotallennusavaimen SWEETALERT_HISTORY_RESTORED_KEY joka on asetettu, kun historia on palautettu. Jos näin on, suljemme modaalin välittömästi (se säästää HTMX:n sotkemasta meitä oudolla historianhallinnalla).

Järjestämme myös tapahtuman sweetalert:opened Joilla voit tehdä mitä tahansa custom-logiikkaa, jota tarvitset.

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;
    }
});

Lisäksi asetamme aikalisän tapausten käsittelylle, joissa pyyntö on paikallaan. Tämä on tärkeää, koska HTMX ei aina sulje liikennemuotoa, jos pyyntö epäonnistuu (etenkin jos käytät hx-boost). Tämä on tässä. const SWEETALERT_TIMEOUT_MS = 10000; // Timeout for automatically closing the loader Voimme siis sulkea modaalin, jos jokin menee pieleen (se myös kirjautuu konsoliin).

Suljen sen

Joten nyt kun liikennemuoto on auki, se on suljettava, kun pyyntö on valmis. Tätä varten kutsumme maybeClose Funktio. Tätä kutsutaan, kun pyyntö on valmis (joko onnistuneesti tai erehdyksessä). Käyttäminen htmx:afterRequest sekä htmx:responseError tapahtumat. Nämä tapahtumat syttyvät, kun HTMX on saanut pyynnön valmiiksi (huomautus, nämä ovat tärkeitä, erikoisuuksia HX-Boost joka voi olla vähän hassua siitä, mitä tapahtumia se sytyttää.)

    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);
        }
    }

Näet tämän funktion tarkistavan, onko istuntotallennuksessa oleva polku sama kuin pyynnön polku. Jos on, se sulkee liikennemuodon ja poistaa reitin istuntotallennuksesta.

Historiaa

HTMX:llä on hatara tapa käsitellä historiaa, mikä voi jättää modaalin "sokan" auki takasivulla. Joten lisäämme tähän vielä pari tapahtumaa (useimmiten tarvitsemme vain yhden, mutta vyön & raudat).

    //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');
    });`

Huomaat, että asetamme myös sessionStorage.setItem(SWEETALERT_HISTORY_RESTORED_KEY, 'true'); Jonka me tarkistamme didOpen tapahtuma:

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

Teemme sen silloin, kun liikennemuoto ei aukea heti. popstate \ htmx:historyRestore (varsinkin, jos sinulla on paljon historiaa). Joten meidän täytyy tarkistaa, että didOpen Tapahtuma (silloin kun se on istuntoavaimessa, joskus tämä voi ladata uudelleen jne., joten meidän täytyy olla tietoisia siitä).

Johtopäätöksenä

Näin voit siis käyttää SweetAlert2:ta HTMX-latausmittarina. Se on hieman hakkeri, mutta se toimii, ja se on mukava tapa käyttää samaa kirjastoa sekä latausmittareihin että varmistusikkunoihin.9

logo

©2024 Scott Galloway