# 使用 SweetAlert2用于 HTMX 加载指示器 @ (hx}-indicator@)

<datetime class="hidden">2025-04-21T20:05</datetime>

<!--category-- Javascript, HTMX -->
# 一. 导言导言 导言 导言 导言

一个工作项目I' 一直使用和滥用HTMX来建造一个管理员UíMS K1 作为这个工程的一部分 [SweetAlert 2 * 甜蜜的阿勒特* MSK0*](https://sweetalert2.github.io/#examples) 工具文件库 [用于确认对话框](https://www.mostlylucid.net/blog/showingtoastandswappingwithhtmx#additional-htmx-features).}它的工作很棒 但我也想用它们来替代我的HTMX装货指示器

这证明是一个挑战 所以我想I'd 将它记录在这里 来给你省下同样的痛楚

 <p style="color:red">Warning I'm a C# coder my Javascript is likely horrible.</p>
[TOC]

# 问题

所以HTMX非常聪明 , 它很聪明 `hx-indicator` 通常您可以在您的页面中为 HTMX 请求设置一个装入指示@. @% 通常这是 HTML 元素, 如

```html

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

然后当您想要使用时, 您将您的 HTMX 请求安装为 `hx-indicator="#loading-modal"` 并将显示请求在何时进行中的模式@( @%[详情请见此](https://htmx.org/attributes/hx-indicator/)).

现在HTMX使用 `request` 对象对象 [它在内部追踪](https://github.com/bigskysoftware/htmx/blob/86893ebf4cb0f38484a522044f9a07cdd79398fa/src/htmx.js#L3293)

```javascript
  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
  }

```

因此,我们用它来替换这些请求有点困难: . @ 如何追踪请求, 然后在请求进行中显示 SweetAlert2}模式, 当请求完成后隐藏它@ MS K3}

# 解决方案

所以,我设置了“(”而不是因为我得去, 因为我需要‘:))’来用一个 SweetAlert2}#modal+.代替HTMX装载指示器。
不管怎么说 '是我想出代码的

您在 HTML {(} 脚本中导入 SweetAlert} 2} , 您可以从您输入 sweetAlert@ msk1} 作为脚本 *&} 样式标签 @ MS K4}\ @ @ MPK5} 以 Webpack 或类似的 < ( > 的方式导入它[可以看到他们的医生为此@). @ @
](https://sweetalert2.github.io/#download)

在 npm 安装之后, 您可以在您的 JS 文件“ . ” 中导入这样的文件。 @ info/ plain

```javascript
import Swal from 'sweetalert2';
```

然后,我的主要代码看起来像这个:

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

您在您的 Employ 中使用 OFM @ )设置了此“ (” 配置 。 `main.js` 像这样的文件

```javascript

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

### 寻找我们的元素

你瞧,我用的是这个 `getIndicatorSource` 找到触发 HTMX 请求的元素@ .} 这很重要, 因为我们需要知道哪个元素触发了请求, 这样当它完成后我们就可以关闭模式@ I' @ MS K2} 这是很重要的, 因为HTMX有 MPK3 instinheritance_ ' 因此您需要爬上树来寻找触发请求的要件\ .}

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

```

然后在任何HTMX要求时 @ (so `hx-get` 或 `hx-post`) 您可以使用 `hx-indicator` 指定 SweetAlert@2 @ medal.}您甚至不需要指定先前的类 *{,} 仅指定现有工程中的参数=.}

让我们看看这一切是如何运作的 :

## 勾搭起来 ` registerSweetAlertHxIndicator()`

这表现为切入点.}你可以看到它插在 `htmx:configRequest` 当 HTMX 即将提出请求时, 这是被发射的@. @%

然后它得到触发事件元素 `evt.detail.elt` 或检查,如果它有, `hx-indicator` 属性@. @%

显示SweetAlert@2+#模式使用 `Swal.fire()`.

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

```

## 获取请求路径

如果使用“ ,” 它会得到请求路径 `getRequestPath(evt.detail)` 并储存在会话存储库中
Niw HTMX是一个狡猾的鸡巴,}它保存路径在不同的地方 取决于你在生命周期中的位置 @.}所以在我的代码里,我做所有的 Above.用 `detail?.pathInfo?.path ?? detail?.path ?? '';`

结果发现HTMX储存了 *请求要求* 路径中 `detail.path` 和响应路径@(} `document.body.addEventListener('htmx:afterRequest', maybeClose); document.body.addEventListener('htmx:responseError', maybeClose);`) 加入 `detail.PathInfo.responsePath` 所以我们需要处理两个问题

我们还需要处理 `GET` 窗体@;}{} ,因为它们的回复可能包含 URL 元素。 `<input >` 值, 这样响应URL 最终会变成不同的@ . @ @

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

注:: 使用 `HX-Push-Url` 更改 HTMX 为历史存储HTMX 请求的 URL\ .

### 表格形式

`HttpGet` 窗体是有点困难的,所以我们有一个代码 如果点击 a `submit` 在窗体中的按钮中并附加由这些 pesky 输入导致的查询字符串参数,以便与回应 URL @. 比较

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

    }
}
```

第二个是这里的:

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

## 正在存储路径

好了,现在我们有了路径 , 我们用它做什么? MSSK1 好追踪哪个请求触发了 SweetAlert2模式 `sessionStorage` 使用 `sessionStorage.setItem(SWEETALERT_PATH_KEY, path);`.

(#AA Agin, 您可以让这个过程变得更复杂, 确保您只有在需要时才能拥有它@.)}

## 显示模式

然后我们只需展示 SweetAlert 2 模式使用 `Swal.fire()`.注意到我们这里有一大堆选择

打开时检查会话存储密钥 `SWEETALERT_HISTORY_RESTORED_KEY` 当历史被恢复时设置@. @ 如果它是 ,} 我们立即关闭模式 *#(* 它会保存 HTMX 干扰我们使用它 '{怪异的历史管理}).}

我们亦纵火, `sweetalert:opened` 用于做任何自定义逻辑的“.”

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

此外,我们设置了一个超时时间来处理请求挂挂@ .的案件。 这很重要 因为 HTMX does '}如果请求失败了 @(, 特别是您使用 `hx-boost`). 设置在此 `const SWEETALERT_TIMEOUT_MS = 10000; // Timeout for automatically closing the loader` 因此,如果出了什么问题,我们可以关闭模式。 @(it' 还会登录到控制台@).

## 关门了

所以现在我们有了模式 打开, 我们需要关闭它 当请求完成时 `maybeClose` 函数@.}这在请求完成时被调用 * }% 1 成功或出错 *\ MPK2}
使用 `htmx:afterRequest` 和 `htmx:responseError` HTMX 完成请求后发生火灾 `HX-Boost` 可能有些有趣的是,

```javascript
    document.body.addEventListener('htmx:afterRequest', maybeClose);
    document.body.addEventListener('htmx:responseError', maybeClose);
```

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

您将看到此函数检查会话存储的路径是否与请求的路径相同@.}如果它是 @,, 它关闭了模式并删除会话储存的路径\ .}

## Itó'历史

HTMX有处理历史的绝妙方法, 它可以让模式@' @stuck'}在做后页时打开 .}(所以我们再增加几个事件来抓住这个#() 。 我们只需要一条皮带而已。

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

你们看,我们也设置了 `sessionStorage.setItem(SWEETALERT_HISTORY_RESTORED_KEY, 'true');` 我们检查在 `didOpen` 事件@: @

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

我们这样做的事件发生时, 模式是 '#t alwasy即刻打开 `popstate` \ `htmx:historyRestore` 特别是如果你有很多历史记录 所以我们需要检查一下 `didOpen` 事件@( @ huh it is in session key@ ,}有时这可以重新加载等Q...}所以我们必须意识到

# BONUS @ - 用于我的 PagingTagHelper

如果你用我的 [Paging 塔帮助器](https://www.mostlylucid.net/blog/category/PagingTagHelper) {\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080}所以 `PageSize` 并使用 SweetAlert\;}由于它还勾上 `  `所以我们需要添加一个小代码以确保它与SweetAlert@2 medal @.}的冲突。

```javascript
(() => {
    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;
})();
```

要修正此功能, 您需要添加一个小函数@ : @

```javascript
/**
 * 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;
}
```

这只是得到了请求 并补充了 `pagesize` 参数,然后导入此函数

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

```

你可以看到,我们在这里发现 `pageSize` 当它发现时 ( @% 因此您更改了页面@)} 它使用 `getPathWithPageSize` 函数以获取新路径@.}The `pageSize` 代码将会在管道中稍后部分替换此代码, 但这样它就能正确检测到它, 因此它会像预期的那样关闭@ . @ {}

# 在结论中的结论

这样您就可以使用 SweetAlert2作为 HTMX 装载指示器@ . @ It\'} 它有点黑 但是它有效 而且它是用同一图书馆来加载指示器和确认对话框的好方法