默认触发事件
每种 HTML 元素都有合理的默认触发事件,大多数情况下不需要显式写 hx-trigger:
<button>、<a>
click — 点击时触发请求<input>、<select>、<textarea>
change — 值改变且失去焦点时触发(与原生 change 事件一致)<form>
submit — 表单提交时触发,自动防止默认页面跳转其他元素
click — 通用默认触发事件hx-trigger 语法结构
hx-trigger="[事件名] [修饰符...], [事件名2] [修饰符...]"
事件名:任意 DOM 事件(click、keyup、change、submit、load、revealed…)
或 htmx 内置触发器(every Ns、load、revealed)
修饰符:once | changed | delay:Xms | throttle:Xms | from:selector |
target:selector | consume | queue:[first|last|none|all]
常用修饰符详解
once
只触发一次,之后永远不再触发。常用于"首次加载"场景,避免重复请求。
changed
只有当元素的值真正变化时才触发(对于输入框,避免相同值重复请求)。
delay:Xms
防抖(Debounce):事件触发后等待 X 毫秒,如果期间没有再次触发才发出请求。搜索框的理想设置,避免每个按键都发请求。
throttle:Xms
节流(Throttle):每 X 毫秒内最多触发一次。与 delay 的区别:throttle 会立即发第一次请求,然后限制频率;delay 是等待停顿后才发。
from:selector
监听其他元素上的事件,而非触发元素本身。常用于跨元素联动,或监听 document/window 上的全局事件。
target:selector
只有当事件的目标元素匹配选择器时才触发(事件委托)。用于动态生成的子元素的事件处理。
consume
阻止事件继续冒泡(stopPropagation),防止父元素的 htmx 触发器响应同一事件。
queue:策略
控制请求队列:
first(忽略新请求,等当前完成)、last(取消当前,响应最新)、none(忽略新请求)、all(所有请求都排队执行)。<!-- 防抖:输入停止 300ms 后才搜索 -->
<input
name="q"
hx-get="/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#results"
placeholder="实时搜索..."
/>
<!-- 节流:滚动时每 200ms 最多发一次请求 -->
<div
hx-get="/api/scroll-position"
hx-trigger="scroll from:window throttle:200ms"
hx-swap="none"
></div>
<!-- 只触发一次:懒加载评论区 -->
<div
id="comments"
hx-get="/api/comments"
hx-trigger="revealed once"
hx-target="this"
>
<p>评论加载中...</p>
</div>
<!-- from:监听另一个元素上的事件 -->
<button id="refresh-btn">刷新</button>
<div
hx-get="/api/data"
hx-trigger="click from:#refresh-btn"
hx-target="this"
></div>
轮询:every N seconds
htmx 内置支持轮询(Polling),无需 setInterval,用 HTML 属性就能实现定时刷新:
<!-- 每 2 秒刷新一次服务器状态 -->
<div
hx-get="/api/server-status"
hx-trigger="every 2s"
hx-target="this"
>
<p>检测中...</p>
</div>
<!-- 首次立即加载,之后每 5 秒刷新 -->
<div
hx-get="/api/notifications"
hx-trigger="load, every 5s"
hx-target="#notif-list"
></div>
<div id="notif-list"></div>
停止轮询的技巧
服务器可以通过在响应中返回 HTTP 286 状态码来让 htmx 停止轮询——这在任务完成、错误发生或用户不在页面时非常有用。
# Python:任务完成后停止轮询
if task.is_complete():
return HTMLResponse("<p>任务完成!</p>", status_code=286)
load 与 revealed:懒加载
load
元素被加载到 DOM 后立即触发一次。常用于初始化加载(比如弹窗打开后加载内容)。与
once 配合使用是最佳实践。revealed
元素滚动进入视口时触发。基于
IntersectionObserver 实现,比 scroll 事件更高效。是无限滚动、图片懒加载的理想方案。intersect
类似
revealed,但可以更精细地控制触发阈值,如 intersect threshold:0.5(元素 50% 可见时触发)。<!-- 骨架屏 + 懒加载 -->
<div class="product-grid">
<!-- 已渲染的前 10 条 -->
<div class="product-card">...</div>
<!-- 滚动到底部时自动加载更多 -->
<div
class="load-more-trigger"
hx-get="/api/products?page=2"
hx-trigger="revealed"
hx-target="closest .product-grid"
hx-swap="beforeend"
>
<div class="skeleton"></div>
</div>
</div>
<!-- 服务器返回的内容(包含下一页触发器) -->
<!-- products_page_2.html:
<div class="product-card">...第11条...</div>
<div class="product-card">...第12条...</div>
...
<!-- 如果还有更多 -->
<div class="load-more-trigger"
hx-get="/api/products?page=3"
hx-trigger="revealed"
hx-target="closest .product-grid"
hx-swap="beforeend">
<div class="skeleton"></div>
</div>
-->
多事件与自定义事件
hx-trigger 可以监听多个事件(逗号分隔),也可以监听自定义 JavaScript 事件:
<!-- 多事件:点击或者接收到自定义事件都触发 -->
<div
hx-get="/api/notifications"
hx-trigger="click, newNotification from:body"
hx-target="#notif-panel"
>通知</div>
<!-- 通过 JavaScript 手动触发 htmx 请求 -->
<script>
// 触发自定义事件,让上面的 div 发出请求
document.body.dispatchEvent(new Event('newNotification'));
// 或使用 htmx API 直接触发元素上的请求
htmx.trigger(document.getElementById('my-element'), 'htmx:load');
</script>
<!-- 事件过滤:只有当 Ctrl 键按下时才触发 -->
<button
hx-delete="/api/all-data"
hx-trigger="click[ctrlKey]"
>Ctrl+Click 删除所有数据</button>
<!-- keyup 过滤:只响应 Enter 键 -->
<input
hx-post="/api/search"
hx-trigger="keyup[key=='Enter']"
hx-target="#results"
/>
事件过滤语法
hx-trigger="event[condition]" 中,方括号内是任意 JavaScript 表达式,在事件对象的上下文中执行(可访问 event、ctrlKey、shiftKey、key、target 等属性)。只有条件为真时,htmx 才发出请求。
hx-trigger 综合示例:实时用户名检查
<div class="form-field">
<label>用户名</label>
<input
type="text"
name="username"
hx-get="/api/check-username"
hx-trigger="keyup changed delay:400ms"
hx-target="next .validation-msg"
hx-indicator="next .checking"
minlength="3"
maxlength="20"
/>
<span class="checking htmx-indicator">检查中...</span>
<span class="validation-msg"></span>
</div>
@app.get("/api/check-username")
async def check_username(username: str):
if len(username) < 3:
return HTMLResponse('<span class="error">用户名至少 3 个字符</span>')
exists = await db.user_exists(username)
if exists:
return HTMLResponse('<span class="error">❌ 用户名已被占用</span>')
return HTMLResponse('<span class="success">✓ 用户名可用</span>')