Back to "将 SweetAlert2 用于 HTMX 装入指标(hx-indicator)"

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

HTMX Javascript

将 SweetAlert2 用于 HTMX 装入指标(hx-indicator)

Monday, 21 April 2025

一. 导言 导言 导言 导言 导言 导言 一,导言 导言 导言 导言 导言 导言

我用HTMX做一个工作项目 并滥用HTMX来建造一个行政UI 作为其中一部分,我正在使用可爱的 甜加速器2color Javascrap 库库 用于确认对话框的确认对话框.. 效果很好,但我还想用它们来替代我的HTMX装货指标。

这证明是一个挑战 所以我想把文件记录在这里 来给你省下同样的痛苦

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

[TOC]

问题

所以HTMX很聪明 hx-indicator 通常允许您为 HTMX 请求设定装入指标。 通常这是您页面中的 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" 并在请求提出时显示模式(见A/CN.9/WG.III/WP.III/WP.1号文件)。详情请见此).

现在HTMX使用 request 对象对象 它在内部追踪

  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 模式, 然后在请求完成后隐藏它 。

解决方案

所以,我设置了(不是因为我不得不,因为我需要:)用SweetAlert2模式取代HTMX装载指标。 不管怎么说,这是我要的密码

您可以从在 HTML 中导入 SweetAlert2 开始( 作为脚本和样式标签) / 为 Webpack 或类似( 类似) 导入它 (SweetAlert2) 。看见他们的医生就为这个。

在 npm 安装之后, 您可以在您的 JS 文件中导入这样的文件 。

import Swal from 'sweetalert2';

我的主要代码是这样的:

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

您在您的 main.js 像这样的文件


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

寻找我们的元素

你会看到我用 getIndicatorSource 函数以查找触发 HTMX 请求的元素。 这一点很重要,因为我们需要知道 哪个元素触发了请求 这样我们就可以关闭模式 当它完成。 这一点很重要,因为HTMX有“遗传性”,所以你需要爬上树,找到触发请求的元素。

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

然后,应任何HTMX要求, hx-gethx-post您可以使用 hx-indicator 要指定 SweetAlert2 模式的属性。 你甚至不需要像以前那样指定类别, 只需要现有的参数工作 。

让我们来回顾一下这一切是如何运作的:

勾搭起来 registerSweetAlertHxIndicator()

这起起作为切入点的作用。 你可以看到它与 htmx:configRequest 事件 。 当HTMX 即将提出请求时,它就会被发射。

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

最后,它展示了SweetAlert2模式使用 Swal.fire().

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是个狡猾的混蛋, 它将路径存放在不同的地方, 取决于您在生命周期中的位置。 所以在我的代码里,我做所有的事情。 与 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 最终会变得不同。

// 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 存储历史请求的 URL 标题。

表格形式

HttpGet 窗体是有点困难的,所以我们有一个代码, 如果你点击 submit 在窗体中键并附加由这些 pesky 输入导致的查询字符串参数,以便与响应 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"
        }));

    }
}

第二个是在这里处理的:

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

正在存储路径

好,现在我们有路了,怎么办? 来追踪哪个请求触发了SweetAlert2模式 我们把它储存在 sessionStorage 使用 sessionStorage.setItem(SWEETALERT_PATH_KEY, path);.

(你可以让事情变得更复杂,并确保只有需要时才能有。 )

显示模式

然后我们只需展示 SweetAlert2 模式使用 Swal.fire().. 注意我们这里有很多选择

打开时检查会话存储密钥 SWEETALERT_HISTORY_RESTORED_KEY 当历史被恢复时,该历史将被设置。 如果是这样的话,我们马上关闭模式(这可以避免HTMX干扰我们使用奇特的历史管理)。

我们亦纵火, sweetalert:opened 它可以用来做任何你需要的自定义逻辑。

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如果请求失败(特别是如果您使用 hx-boost) ) 此设置在此 const SWEETALERT_TIMEOUT_MS = 10000; // Timeout for automatically closing the loader 这样我们就可以关闭模式 如果出事了(它也会登录到控制台)

关门了

所以,现在我们打开了模式, 我们需要在请求完成后关闭它。 为此,我们称之为 maybeClose 函数。 当请求完成( 成功或有错误) 时呼叫此选项 。 使用 htmx:afterRequesthtmx:responseError 活动。 一旦HTMX完成一项请求,这些事故一旦发生,HTMX就火灾(请注意,这些非常重要,特别是: HX-Boost 可能有些有趣的是它会点燃什么。 ))

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

您会看到此功能检查会话存储路径是否与请求路径相同 。 如果是的话,它关闭模式,从会话存储中移除路径。

这是历史

HTMX有处理历史的密不可分的方法 它可以在做后页时打开模型“stuck”的开关。 因此我们再增加几个事件来抓住这个机会(大多数时候我们只需要一个,但带和支架)。

    //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 事件 :

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

我们这样做的万一 模式不打开 立即打开 popstate \ htmx:historyRestore (特别是如果你有很多历史的话) 所以我们需要检查一下 didOpen 事件(因此它是在会话键, 有时它可以重新装入等等... 所以我们必须意识到这一点) 。

在结论结论中

这就是你如何使用SweetAlert2作为HTMX载荷指示器。 这是一个有点黑客,但它是有效的, 它是一个很好的方法 使用同一个图书馆 装载指示器和确认对话框。

logo

©2024 Scott Galloway