Chapter 05

指示器、确认与进度反馈

为 htmx 请求添加加载状态反馈,防止用户重复提交,构建可预期的交互体验

htmx-indicator:加载状态指示器

工作机制

htmx 在发出请求的期间,会自动向触发元素及其最近的父元素添加 htmx-request CSS 类;同时,会向 hx-indicator 指向的元素(或触发元素自身)添加 htmx-request 类并显示它。

默认情况下,带有 htmx-indicator 类的元素是不可见的(opacity: 0),当请求进行中时变为可见(opacity: 1)。

/* htmx 内置的 CSS(简化版)*/
.htmx-indicator {
  opacity: 0;
  transition: opacity 200ms ease-in;
}

.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
  opacity: 1;
}

/* 请求时按钮变灰 */
.htmx-request {
  cursor: not-allowed;
  opacity: 0.6;
}

基础用法

<!-- 方式1:指示器是触发元素的后代 -->
<button hx-get="/api/data" hx-target="#result">
  <span>加载数据</span>
  <span class="htmx-indicator"></span>
</button>

<!-- 方式2:hx-indicator 指向外部元素 -->
<button
  hx-get="/api/data"
  hx-target="#result"
  hx-indicator="#global-spinner"
>加载</button>

<div id="global-spinner" class="htmx-indicator spinner">
  加载中...
</div>

<!-- 方式3:表单提交时按钮显示加载状态 -->
<form hx-post="/api/save">
  <input name="title" required/>
  <button type="submit">
    <span class="btn-text">保存</span>
    <span class="htmx-indicator">保存中...</span>
  </button>
</form>

自定义加载动画

/* CSS 旋转动画 spinner */
.spinner {
  display: inline-block;
  width: 16px; height: 16px;
  border: 2px solid rgba(255,255,255,0.3);
  border-top-color: #fff;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* 全局顶部进度条(类似 GitHub/YouTube) */
#progress-bar {
  position: fixed; top: 0; left: 0;
  height: 3px; background: #3d9cf0;
  width: 0; transition: width 0.3s;
  z-index: 9999;
}

.htmx-request #progress-bar {
  width: 70%;
  transition: width 2s ease-out;
}

hx-confirm:确认对话框

hx-confirm 在请求发出前弹出一个原生 confirm() 对话框。只有用户点击"确定"后才发出请求,点击"取消"则中断操作。

<!-- 简单确认 -->
<button
  hx-delete="/api/account"
  hx-confirm="确定要永久删除账号吗?此操作无法撤销!"
>
  删除账号
</button>

<!-- 使用模板语法(需要服务端或 JS 动态生成) -->
<button
  hx-delete="/api/files/report.pdf"
  hx-confirm="确认删除文件 report.pdf?"
  hx-target="closest .file-row"
  hx-swap="outerHTML"
>
  删除
</button>

自定义确认对话框(替代原生 confirm)

原生 confirm() 样式简陋,htmx 支持通过事件拦截实现自定义对话框:

<!-- 自定义对话框 -->
<dialog id="confirm-dialog">
  <p id="confirm-msg"></p>
  <button id="confirm-yes">确认</button>
  <button onclick="this.closest('dialog').close()">取消</button>
</dialog>

<script>
// 拦截 htmx 的确认事件,显示自定义对话框
document.addEventListener('htmx:confirm', function(e) {
  e.preventDefault(); // 阻止默认的 confirm() 弹窗

  const dialog = document.getElementById('confirm-dialog');
  document.getElementById('confirm-msg').textContent = e.detail.question;

  dialog.showModal();

  document.getElementById('confirm-yes').onclick = function() {
    dialog.close();
    e.detail.issueRequest(true); // 真正发出请求
  };
});
</script>

hx-disable:禁用处理

hx-disable 属性告诉 htmx 忽略该元素及其子元素上的所有 hx-* 属性,就像 htmx 不存在一样。这对于在某些区域保持传统表单行为、或者在内容编辑器中防止意外触发非常有用:

<!-- 整个区域禁用 htmx(如富文本编辑器内容) -->
<div hx-disable>
  <!-- 这里的 hx-* 属性完全被忽略 -->
  <button hx-get="/this-will-NOT-work">不会触发</button>
</div>

<!-- 排除某个表单不使用 htmx,保留原生提交 -->
<body hx-boost="true">
  <!-- 大多数表单通过 htmx 提交 -->
  <form hx-post="/api/data">...</form>

  <!-- 这个表单使用传统提交(文件下载、OAuth 等) -->
  <form action="/auth/google" method="POST" hx-disable>
    <button>Google 登录</button>
  </form>
</body>

请求队列管理

当用户快速多次触发同一个请求时(如连续点击按钮),htmx 的队列策略决定如何处理并发请求:

queue:first
锁定当前请求,忽略所有新请求,直到当前请求完成。适合"提交"按钮(防止重复提交)。
queue:last
取消当前等待中的请求,用最新的请求替代。适合搜索框(用户不断输入,只关心最后一次结果)。
queue:none
当前有请求进行中时,直接丢弃新请求(不等待也不替换)。
queue:all
所有请求都排队依次执行。适合需要保证顺序的操作(如多步骤向导)。
<!-- 防重复提交:第一次点击后锁定,直到服务器响应 -->
<button
  hx-post="/api/order/submit"
  hx-trigger="click queue:first"
  hx-indicator="#submit-spinner"
>
  下单
  <span id="submit-spinner" class="htmx-indicator">处理中...</span>
</button>

<!-- 搜索框:只保留最新输入的搜索 -->
<input
  hx-get="/search"
  hx-trigger="keyup delay:200ms queue:last"
  hx-target="#results"
/>

htmx 生命周期事件

htmx 在请求生命周期的各阶段触发自定义 DOM 事件,可以用 JavaScript 监听这些事件实现精细控制:

htmx:beforeRequest
请求发出前触发。调用 event.preventDefault() 可以取消请求。
htmx:afterRequest
请求完成后触发(无论成功还是失败)。
htmx:beforeSwap
DOM 更新前触发。可以修改响应内容或取消更新。
htmx:afterSwap
DOM 更新后触发。新内容已在页面中,可以进行后续初始化。
htmx:responseError
服务器返回 4xx/5xx 状态码时触发。默认 htmx 不更新 DOM,此时可以显示错误提示。
<script>
// 全局错误处理:服务器错误时显示 toast 通知
document.addEventListener('htmx:responseError', function(e) {
  const status = e.detail.xhr.status;
  const msg = status === 422
    ? '数据验证失败,请检查输入'
    : status === 403
    ? '没有权限执行此操作'
    : `请求失败 (${status})`;
  showToast(msg, 'error');
});

// 请求完成后重新初始化第三方组件
document.addEventListener('htmx:afterSwap', function(e) {
  // 例如重新初始化 Prism.js 代码高亮
  if (typeof Prism !== 'undefined') {
    Prism.highlightAllUnder(e.detail.elt);
  }
});
</script>
处理 4xx 响应中的 HTML

htmx 默认只在 2xx 响应时更新 DOM(因为 4xx/5xx 表示错误)。若希望 422 验证错误的响应内容(错误提示 HTML)也能更新 DOM,需要在 htmx:beforeSwap 事件中手动允许:

document.addEventListener('htmx:beforeSwap', function(e) {
  if (e.detail.xhr.status === 422) {
    e.detail.shouldSwap = true; // 允许更新 DOM
    e.detail.isError = false;  // 不触发 responseError
  }
});
本章小结

htmx 事件系统与 JavaScript 集成的核心要点:① htmx 生命周期事件遵循 htmx:beforeRequest → htmx:afterRequest → htmx:afterSwap → htmx:afterSettle 顺序,可在任意环节挂载行为;② hx-on::after-request 是内联事件的简写语法(双冒号 = htmx: 前缀),适合简单操作如提交后清空表单;③ htmx.trigger(el, 'eventName') 可从 JS 主动触发元素的 htmx 请求;htmx.ajax() 可完全用 JS 控制 htmx 请求;④ 服务器通过 HX-Trigger 响应头触发客户端事件,实现后端→前端通信(如弹出 Toast 通知);⑤ htmx 默认只在 2xx 时 swap DOM;422 验证错误需在 htmx:beforeSwap 中手动设置 shouldSwap = true;⑥ htmx:afterSwap 事件中重新初始化第三方库(如代码高亮、日期选择器),因为新注入的 DOM 不会自动触发库的初始化。