mcpField
D
drupal-htmx

drupal-htmx

github.com/edutrul開発7361 回インストール21 週間前に更新
リポジトリ

drupal-htmx について

Drupal 11.3 以降で、追加モジュールなしにコアの HTMX を使うための開発リファレンスです。PHP ビルダーやレンダリング動作に加え、部分更新、ナビゲーション、動的 UI の実装パターンを扱います。

主な機能

  1. 01コア HTMX ライブラリの追加
  2. 02PHP でリクエスト属性を構築
  3. 03部分ルート応答のレンダリング
  4. 04ブーストナビゲーションへの対応
  5. 05遅延読み込みと無限スクロール
  6. 06Drupal behaviors と設定の連携

活用シーン

  1. 01連動するフォーム項目の更新
  2. 02ページ全体を再読み込みせず断片を取得
  3. 03Drupal のルートやフォームに HTMX 操作を追加

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/edutrul/drupal-ai --skill drupal-htmx

インストール後、Claude Code で/drupal-htmxを入力して読み込まれたか確認します。

同梱ファイルREADME.md

詳細

Drupal HTMX

HTMX 2.0.4 は Drupal 11.3 以降のコアに core/htmx として含まれています。contrib モジュールは必要ありません。

HTMX と AJAX Form API の使い分け

シナリオ使用するもの
他のフィールドを更新するフォームフィールドHTMX(swapOob)
部分的なページ更新を行うシンプルなフォーム送信HTMX または AJAX Form API
AjaxResponse コマンドを使用する複雑な複数ステップの AJAXAJAX Form API
フォーム以外の UI 操作(遅延読み込み、無限スクロール、boost)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-* 属性を直接書き込みます。代わりにラッパー div を対象にするには、2 番目の引数として '#wrapper_attributes' を渡します。

$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

パターン:連動するセレクト(動的フォーム)

ページを再読み込みせずに別のセレクトを更新するセレクトです。両方のフィールドをアウトオブバンドで再レンダリングするため、各フィールドは swapOob を使って <current> に POST します。

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 レイアウトのバイパス

HTMX 要素に data-hx-drupal-only-main-content を追加します。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>

パターン:Boost(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 では hx-get ではなく data-hx-get と記述してください。

oob トリガーで swap('none') を忘れる: swapOob を使用すると、swap('none') を設定しない限り、トリガー要素自体もスワップされます。

POST での CSRF: Drupal の Form API が CSRF トークンを処理します。コントローラーへのフォーム以外の HTMX POST では、ルート要件に _csrf_token: 'TRUE' を追加し、URL に ?token={{ drupal_csrf_token(...) }} を付加してください。

ターゲットのプレースホルダーに生のマークアップを使用する: 単独の HTMX ターゲット div をレンダリングするために、#markup は絶対に使用しないでください。Drupal のレンダリングパイプラインを迂回してしまいます。代わりに、#attributes とともに #type => 'html_tag' を使用してください。

// Bad
'target' => ['#markup' => '<div id="greeting-target"></div>'],

// Good
'target' => [
  '#type' => 'html_tag',
  '#tag' => 'div',
  '#value' => '',
  '#attributes' => ['id' => 'greeting-target'],
],

既存のレンダー要素のラッパーをターゲットにできる場合は、別個の div を省略し、代わりに '#wrapper_attributes' とその要素の #attributes['id'] を指定して applyTo() を使用してください。

コメント