mcpField
D
drupal-htmx

drupal-htmx

github.com/edutrul开发7361 次安装21 周前更新
代码仓库

关于 drupal-htmx

面向 Drupal 11.3+ 的开发参考,介绍如何直接使用核心中的 HTMX,无需贡献模块。内容涵盖 PHP 构建器、渲染行为,以及局部更新、导航和动态界面的实现模式。

主要功能

  1. 01附加核心 HTMX 库
  2. 02用 PHP 构建请求属性
  3. 03渲染局部路由响应
  4. 04支持增强式导航
  5. 05处理延迟加载和无限滚动
  6. 06集成 Drupal behaviors 与设置

适用场景

  1. 01更新相互依赖的表单字段
  2. 02无需整页刷新即可加载页面片段
  3. 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 命令的复杂多步骤 AJAXAJAX 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']。

评论

支持 Markdown