关于 drupal-htmx
面向 Drupal 11.3+ 的开发参考,介绍如何直接使用核心中的 HTMX,无需贡献模块。内容涵盖 PHP 构建器、渲染行为,以及局部更新、导航和动态界面的实现模式。
主要功能
- 01附加核心 HTMX 库
- 02用 PHP 构建请求属性
- 03渲染局部路由响应
- 04支持增强式导航
- 05处理延迟加载和无限滚动
- 06集成 Drupal behaviors 与设置
适用场景
- 01更新相互依赖的表单字段
- 02无需整页刷新即可加载页面片段
- 03为 Drupal 路由和表单添加 HTMX 交互
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ edutrul/ drupal-ai --skill drupal-htmx安装后在 Claude Code 中输入/drupal-htmx确认已加载。
附带文件README.md
详细介绍
Drupal HTMX
Drupal 11.3+ 核心内置 HTMX 2.0.4,位于 core/htmx。无需 contrib 模块。
何时使用 HTMX,何时使用 AJAX Form API
| 场景 | 使用 |
|---|---|
| 会更新其他字段的表单字段 | HTMX(swapOob) |
| 简单表单提交并局部更新页面 | HTMX 或 AJAX Form API |
使用 AjaxResponse 命令的复杂多步骤 AJAX | AJAX Form API |
| 非表单 UI 交互(延迟加载、无限滚动、增强导航) | HTMX |
| 要修改的现有表单 | AJAX Form API(干扰较小) |
附加库
# mymodule.libraries.yml
mymodule/htmx-feature:
dependencies:
- core/drupal.htmx
core/drupal.htmx 包含:htmx 供应商 JS + Drupal behaviors 桥接 + 资源加载器 + drupalSettings 集成。
Htmx PHP Fluent Builder
use Drupal\Core\Htmx\Htmx;
use Drupal\Core\Url;
$htmx = new Htmx();
$htmx->post($url)->target('#my-wrapper')->swap('outerHTML');
$htmx->applyTo($element);
applyTo() 会将 data-hx-* 属性直接写入渲染数组元素。将 '#wrapper_attributes' 作为第二个参数传入,可将属性应用到外层 div。
$htmx->applyTo($form['field'], '#wrapper_attributes');
请求属性
$htmx->get(?Url $url) // data-hx-get
$htmx->post(?Url $url) // data-hx-post
$htmx->put(?Url $url)
$htmx->patch(?Url $url)
$htmx->delete(?Url $url)
$htmx->target('#selector') // data-hx-target
$htmx->swap('outerHTML') // data-hx-swap: innerHTML|outerHTML|beforebegin|afterbegin|beforeend|afterend|delete|none
$htmx->swapOob('true') // data-hx-swap-oob: out-of-band swap
$htmx->select('css selector') // data-hx-select: pick part of response
$htmx->selectOob(['#id:outerHTML']) // data-hx-select-oob
$htmx->trigger('click') // data-hx-trigger
$htmx->vals(['key' => 'val']) // data-hx-vals: extra POST values
$htmx->boost() // data-hx-boost: SPA-like link/form enhancement
$htmx->confirm('Are you sure?')
$htmx->on('click', 'alert()') // data-hx-on:click
$htmx->pushUrl(true) // data-hx-push-url
$htmx->onlyMainContent() // adds data-hx-drupal-only-main-content (triggers HtmxRenderer)
响应标头(应用于渲染数组,并随响应发送)
$htmx->pushUrlHeader($url) // HX-Push-Url
$htmx->redirectHeader($url) // HX-Redirect
$htmx->triggerHeader('myEvent') // HX-Trigger (also accepts array with data)
$htmx->triggerAfterSettleHeader('myEvent') // HX-Trigger-After-Settle
$htmx->refreshHeader(true) // HX-Refresh
// Also: replaceUrlHeader, locationHeader, reswapHeader, retargetHeader, reselectHeader, triggerAfterSwapHeader
模式:依赖选择框(动态表单)
一个选择框无需重新加载页面即可更新另一个选择框。每个字段都会向 <current> 提交,并使用 swapOob,以便在带外重新渲染两个字段。
public function buildForm(array $form, FormStateInterface $form_state, string $type = '', string $value = ''): array {
$formUrl = Url::fromRoute('<current>');
$form['type'] = [
'#type' => 'select',
'#options' => ['a' => 'A', 'b' => 'B'],
'#default_value' => $type,
];
(new Htmx())->post($formUrl)->swap('none')->swapOob('true')->applyTo($form['type']);
$form['value'] = [
'#type' => 'select',
'#options' => $this->getOptionsFor($form_state->getValue('type', $type)),
'#default_value' => $value,
];
(new Htmx())->post($formUrl)->swap('none')->swapOob('true')->applyTo($form['value']);
// Optionally push URL on change:
$trigger = $form_state->getUserInput()['_triggering_element_name'] ?? FALSE;
if ($trigger) {
(new Htmx())->pushUrlHeader(Url::fromRoute('mymodule.form', [...]))->applyTo($form[$trigger]);
}
return $form;
}
public function submitForm(array &$form, FormStateInterface $form_state): void {}
模式:局部路由(_htmx_route)
此路由仅返回内容片段(不包含 <html> 外壳)。用于响应 HTMX hx-get/hx-post 调用的控制器。
# mymodule.routing.yml
mymodule.htmx_partial:
path: '/htmx/content/{id}'
defaults:
_controller: '\Drupal\mymodule\Controller\HtmxController::content'
_title: 'Content'
options:
_htmx_route: true # Uses HtmxRenderer — returns HTML fragment only
requirements:
_permission: 'access content'
id: '\d+'
// Controller returns a render array normally — HtmxRenderer handles the rest.
public function content(int $id): array {
$node = $this->entityTypeManager->getStorage('node')->load($id);
return [
'#theme' => 'mymodule_content_card',
'#node' => $node,
'#cache' => ['tags' => $node->getCacheTags()],
];
}
模式:onlyMainContent() — 绕过 Drupal 布局
将 data-hx-drupal-only-main-content 添加到任意 HTMX 元素。Drupal JS 桥接会自动在请求中追加 ?_wrapper_format=drupal_htmx,触发 HtmxRenderer(不包含页面装饰、区块或区域)。
$htmx = new Htmx();
$htmx->get($url)->onlyMainContent()->target('#content-area')->applyTo($element);
也可以直接在 Twig 中添加该属性:
<a href="{{ path('mymodule.page') }}"
data-hx-get="{{ path('mymodule.page') }}"
data-hx-drupal-only-main-content
data-hx-target="#content-area">
Load content
</a>
模式:增强导航(类似 SPA 的导航)
对容器应用 hx-boost 后,其中的所有链接和表单都会由 AJAX 驱动,并替换 <body>、更新 <title>。每次交换后,Drupal 的 Drupal.behaviors 都会自动重新附加。
$form['#attributes']['data-hx-boost'] = 'true';
// or via Htmx builder:
(new Htmx())->boost()->applyTo($form);
模式:延迟加载 / 无限滚动
{# Load content when element scrolls into view #}
<div data-hx-get="{{ path('mymodule.more_items', {page: page}) }}"
data-hx-trigger="revealed"
data-hx-swap="outerHTML">
<div class="loading-spinner">{{ 'Loading...'|t }}</div>
</div>
模式:执行操作前确认
(new Htmx())
->delete(Url::fromRoute('mymodule.delete', ['id' => $id]))
->target('#item-' . $id)
->swap('outerHTML')
->confirm((string) $this->t('Delete this item?'))
->applyTo($form['delete_button']);
模式:从响应触发自定义 JS 事件
// In controller or form, trigger a JS event after swap:
(new Htmx())
->triggerAfterSettleHeader('mymodule:itemUpdated')
->applyTo($element);
// In your Drupal behavior:
Drupal.behaviors.mymoduleListener = {
attach(context) {
context.addEventListener('mymodule:itemUpdated', (e) => {
// React to HTMX-triggered event
});
}
};
Drupal.behaviors 集成
无需额外设置。core/drupal.htmx 库会自动接入:
htmx:drupal:load→ 在交换的内容上调用Drupal.attachBehaviors()htmx:drupal:unload→ 在交换前调用Drupal.detachBehaviors()
所有现有 JS behaviors 都能在 HTMX 注入的内容上正常运行。
HTMX 响应中的 drupalSettings
HTMX 响应中的 drupalSettings 会由 htmx-utils.js 自动深度合并到当前页面的 drupalSettings 中。无需操作。
在 PHP 中检测 HTMX 请求
use Symfony\Component\HttpFoundation\Request;
// Check if request is from HTMX
$isHtmx = $request->headers->has('HX-Request');
$trigger = $request->headers->get('HX-Trigger-Name'); // triggering element name
$currentUrl = $request->headers->get('HX-Current-URL');
缓存注意事项
HTMX 局部响应会正常经过 Drupal 的渲染缓存。照常应用缓存元数据:
return [
'#theme' => 'mymodule_partial',
'#cache' => [
'tags' => ['node:' . $node->id()],
'contexts' => ['url.query_args'],
'max-age' => 0, // for frequently-changing content
],
];
HtmxRenderer 会自动添加 rendered 缓存标签和必需的缓存上下文。
Twig — 附加 HTMX 属性
{# Attach htmx library #}
{{ attach_library('mymodule/htmx-feature') }}
{# Render an element with htmx attributes (set via PHP Htmx builder) #}
{{ content.my_field }}
{# Or write attributes directly in Twig #}
<button data-hx-post="{{ path('mymodule.action') }}"
data-hx-target="#result"
data-hx-swap="innerHTML">
{{ 'Submit'|t }}
</button>
始终使用 data-hx-*(不要使用不带前缀的 hx-*)——Drupal 的实现采用 data- 前缀约定。
使用 HTMX 的表单路由
URL 中的路由参数会作为额外参数传递给 buildForm():
mymodule.form:
path: '/mymodule/form/{type}/{value}'
defaults:
_form: '\Drupal\mymodule\Form\DependentSelectForm'
_title: 'My Form'
type: ''
value: ''
requirements:
_permission: 'access content'
public function buildForm(array $form, FormStateInterface $form_state, string $type = '', string $value = ''): array {
常见错误
缺少库: 未附加 core/drupal.htmx 时,HTMX 属性会被渲染为静态 HTML。请务必在 libraries.yml 或 #attached 中附加该库。
使用裸 hx-*: Drupal 使用 data-hx-*。Htmx 构建器会自动添加 data- 前缀。在 Twig 中,请写 data-hx-get,而不是 hx-get。
在 oob 触发器上忘记设置 swap('none'): 使用 swapOob 时,如果不设置 swap('none'),触发器元素本身也会被替换。
POST 请求中的 CSRF: Drupal 的 Form API 会处理 CSRF 令牌。对于发往控制器的非表单 HTMX POST 请求,请添加路由要求 _csrf_token: 'TRUE',并在 URL 后附加 ?token={{ drupal_csrf_token(...) }}。
使用原始标记作为目标占位元素: 切勿使用 #markup 渲染独立的 HTMX 目标 div,这会绕过 Drupal 的渲染流程。请改用 #type => 'html_tag' 和 #attributes:
// Bad
'target' => ['#markup' => '<div id="greeting-target"></div>'],
// Good
'target' => [
'#type' => 'html_tag',
'#tag' => 'div',
'#value' => '',
'#attributes' => ['id' => 'greeting-target'],
],
如果目标可以是现有渲染元素的包装器,则无需单独的 div,而应在该元素上使用 applyTo() 配合 '#wrapper_attributes',并设置 #attributes['id']。