カスタムブロックに「クリックで開閉する」「タブを切り替える」といった動きを付けたいとき、これまでは view.js で自分で querySelectorAll() と addEventListener() を書くのが定番でした。WordPress 6.5 で入った Interactivity API を使うと、この手続きを data-wp-on--click のような属性(ディレクティブ)とストアの定義に置き換えられます。この記事では、開閉ボックスを題材に必要な設定から状態の持たせ方、PHP 側から値を渡す方法、動かないときの確認どころまでを順に解説します。
目次
Interactivity API が引き受けてくれること
Interactivity API は、フロントエンドの表示を「状態」と「状態に連動する HTML」に分けて書ける仕組みです。HTML 側には data-wp- で始まる属性で「この属性はこの値に連動する」「このイベントでこの処理を呼ぶ」と書き、JavaScript 側にはその値と処理だけを置きます。要素を探して取り出し、クラスを付け外しし、テキストを差し替えるといった DOM 操作のコードは書きません。
ブロック単位で完結するのも利点です。ページに同じブロックが3つ並んでいても、それぞれが自分の状態を持ちます。自前で書く場合はインスタンスごとに要素をまとめて管理する処理が必要でしたが、Interactivity API では要素の階層がそのまま状態の有効範囲になるため、その手間がなくなります。
主なディレクティブ
ディレクティブは HTML の属性として書きます。よく使うものを先に一覧で見ておきます。
| ディレクティブ | 役割 |
|---|---|
data-wp-interactive | その要素以下を Interactivity API の管理下に置き、使うストアの名前空間を指定する |
data-wp-context | その要素以下だけで使えるローカルな状態を JSON で定義する |
data-wp-bind--属性名 | 属性の値を状態に連動させる(hidden や aria-expanded など) |
data-wp-class--クラス名 | クラスの付け外しを状態に連動させる |
data-wp-style--プロパティ名 | インラインスタイルの値を状態に連動させる |
data-wp-text | 要素のテキストを状態に連動させる |
data-wp-on--イベント名 | その要素で起きたイベントにアクションを割り当てる |
data-wp-watch | 参照している状態が変わるたびにコールバックを実行する |
data-wp-init | 要素が現れたときに一度だけコールバックを実行する |
data-wp-each | 配列の要素の数だけテンプレートを繰り返して描画する |
block.json に必要な3つの指定
Interactivity API を使うブロックでは、block.json に3か所の指定が必要です。apiVersion を 3 にすること、supports.interactivity を true にすること、フロント用のスクリプトを viewScript ではなく viewScriptModule で登録することです。
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-plugin/toggle-box",
"title": "開閉ボックス",
"category": "widgets",
"icon": "arrow-down-alt2",
"textdomain": "my-plugin",
"supports": {
"interactivity": true
},
"editorScript": "file:./index.js",
"render": "file:./render.php",
"viewScriptModule": "file:./view.js"
}
supports.interactivity を true にすると、そのブロックの出力に含まれるディレクティブを WordPress がサーバー側で処理してくれます。つまり、最初に表示される HTML の時点で状態が反映された状態になります。「読み込み直後だけ閉じているはずの中身が一瞬見えてしまう」といったちらつきが起きにくいのはこのためです。
viewScriptModule は、フロントで読み込むファイルを「スクリプトモジュール」として登録する項目です。Interactivity API のストアは @wordpress/interactivity から import して使うので、通常の viewScript(従来のスクリプト)では読み込めません。ここを間違えると、ページには何のエラーも出ないまま、ボタンを押しても反応しないという状態になります。
開閉ボックスを作ってみる
ボタンを押すと中身が開き、もう一度押すと閉じるブロックを作ります。動的ブロックとして render.php で HTML を出し、そこにディレクティブを書いていきます。
render.php でディレクティブを出力する
<?php
/**
* 動的ブロックの出力。$attributes / $content / $block が使える。
*/
// このブロック1つぶんの初期状態
$context = array( 'isOpen' => false );
?>
<div
<?php echo get_block_wrapper_attributes(); ?>
data-wp-interactive="my-plugin/toggle-box"
<?php echo wp_interactivity_data_wp_context( $context ); ?>
>
<button
type="button"
class="toggle-box__button"
data-wp-on--click="actions.toggle"
data-wp-bind--aria-expanded="context.isOpen"
data-wp-text="state.buttonLabel"
></button>
<div class="toggle-box__body" data-wp-bind--hidden="!context.isOpen">
<p>ここが開閉する中身です。</p>
</div>
</div>
data-wp-interactive に書いた my-plugin/toggle-box が名前空間です。あとで JavaScript 側の store() に渡す文字列と一致させます。wp_interactivity_data_wp_context() は配列を data-wp-context='{"isOpen":false}' という属性の文字列にして返すヘルパーで、自分で json_encode() してエスケープする必要がありません。
ボタン側では3つのディレクティブを使っています。data-wp-on--click がクリック時に呼ぶアクションの指定、data-wp-bind--aria-expanded が開閉状態を aria-expanded 属性に反映する指定、data-wp-text がボタンのラベルの指定です。中身の div には data-wp-bind--hidden="!context.isOpen" を付けています。ディレクティブの値は先頭に ! を付けて否定できるので、「開いていないときは hidden を付ける」がこの1行で書けます。
view.js でストアを定義する
import { store, getContext } from '@wordpress/interactivity';
store( 'my-plugin/toggle-box', {
state: {
// getter で書くと「他の状態から計算した状態」になる
get buttonLabel() {
return getContext().isOpen ? '閉じる' : '開く';
},
},
actions: {
toggle() {
const context = getContext();
context.isOpen = ! context.isOpen;
},
},
} );
store() の第1引数が名前空間、第2引数が中身です。actions.toggle は getContext() でそのボタンが属するコンテキストを取り出し、isOpen を反転させているだけです。値を書き換えると、それを参照しているディレクティブが自動で描画し直されるので、hidden の付け外しやテキストの差し替えを自分で書く必要はありません。
state.buttonLabel のように getter で定義した値は、参照されるたびに計算される派生状態になります。isOpen が変われば buttonLabel の結果も変わるため、ボタンの文字も一緒に切り替わります。ラベル用の状態を別に持って両方を更新する、といった書き方をしなくて済むということです。
state と context の違い
Interactivity API で値を置く場所は2つあります。混ざりやすいところなので、有効範囲で覚えるのが分かりやすいです。
| 置き場所 | 有効範囲 | 向いている値 |
|---|---|---|
state | 同じ名前空間の中でページ全体に共有される | 翻訳済みのラベル、API の URL、開いているモーダルの ID など全体で1つの値 |
context | data-wp-context を書いた要素とその子孫だけ | ブロック1つごとに別々に持ちたい値(開閉状態、選択中のタブなど) |
同じブロックがページに複数あるとき、開閉状態を state に置いてしまうと全部が同時に開きます。インスタンスごとに独立させたい値は context、という切り分けになります。
PHP から state に値を渡す
ラベルの文字列のように翻訳を通したい値は、JavaScript に直接書くよりも PHP 側から渡したほうが扱いやすくなります。wp_interactivity_state() に名前空間と配列を渡すと、その値が state として JavaScript 側から読めるようになります。
<?php
// 名前空間ごとの state に値を追加する(同じ名前空間なら後から呼んでも合流する)
wp_interactivity_state(
'my-plugin/toggle-box',
array(
'openLabel' => __( '開く', 'my-plugin' ),
'closeLabel' => __( '閉じる', 'my-plugin' ),
)
);
$context = array( 'isOpen' => false );
?>
<div
<?php echo get_block_wrapper_attributes(); ?>
data-wp-interactive="my-plugin/toggle-box"
<?php echo wp_interactivity_data_wp_context( $context ); ?>
>
<!-- 以下は同じ -->
</div>
あとは view.js の getter で、この値を読むように変えるだけです。state の中の他の値は this 経由で参照できます。
store( 'my-plugin/toggle-box', {
state: {
get buttonLabel() {
// this は同じ名前空間の state を指す
return getContext().isOpen ? this.closeLabel : this.openLabel;
},
},
actions: {
toggle() {
const context = getContext();
context.isOpen = ! context.isOpen;
},
},
} );
タブ切り替えを作る
真偽値の切り替えができれば、「選択中のものだけ表示する」形もほぼ同じ書き方で作れます。ここでは context に選択中のタブ名を持たせ、クラスの付け外しと表示の切り替えを行います。
<?php
$tabs = array(
'html' => 'HTML',
'css' => 'CSS',
);
$context = array( 'activeTab' => 'html' );
?>
<div
<?php echo get_block_wrapper_attributes(); ?>
data-wp-interactive="my-plugin/tabs"
<?php echo wp_interactivity_data_wp_context( $context ); ?>
>
<div class="tabs__buttons" role="tablist">
<?php foreach ( $tabs as $key => $label ) : ?>
<button
type="button"
role="tab"
<?php echo wp_interactivity_data_wp_context( array( 'tab' => $key ) ); ?>
data-wp-on--click="actions.selectTab"
data-wp-class--is-active="state.isActive"
data-wp-bind--aria-selected="state.isActive"
><?php echo esc_html( $label ); ?></button>
<?php endforeach; ?>
</div>
<?php foreach ( $tabs as $key => $label ) : ?>
<div
role="tabpanel"
<?php echo wp_interactivity_data_wp_context( array( 'tab' => $key ) ); ?>
data-wp-bind--hidden="!state.isActive"
>
<p><?php echo esc_html( $label ); ?> の内容です。</p>
</div>
<?php endforeach; ?>
</div>
data-wp-context は入れ子にできます。外側で activeTab を定義し、ボタンやパネルごとに tab を追加すると、その要素からは両方が見えます。内側で同じキーを定義すると内側の値が優先されるので、キー名が重ならないようにしておきます。
import { store, getContext } from '@wordpress/interactivity';
store( 'my-plugin/tabs', {
state: {
// 「この要素のタブが選択中か」を返す派生状態
get isActive() {
const { activeTab, tab } = getContext();
return activeTab === tab;
},
},
actions: {
selectTab() {
const context = getContext();
// activeTab は外側のコンテキストにあるが、同じオブジェクトとして書き換えられる
context.activeTab = context.tab;
},
},
} );
同じ state.isActive を、ボタンでは data-wp-class--is-active、パネルでは data-wp-bind--hidden="!state.isActive" として使い回しています。派生状態はそれを参照している要素ごとに評価されるので、「どのタブから見た結果か」は自動的に分かれます。
配列を繰り返して表示する
検索結果のように件数が変わるものは data-wp-each で書きます。template 要素に付け、その中で1件ぶんの表示を書く形です。
<ul>
<template data-wp-each--item="context.items">
<li data-wp-text="context.item"></li>
</template>
</ul>
data-wp-each--item の item の部分が、1件ぶんを受け取る名前です。data-wp-each とだけ書いた場合は context.item になります。サーバー側で描画された初期表示ぶんの要素には data-wp-each-child が付き、これがあることで JavaScript の初期化時に同じ内容を二重に描画せずに済む仕組みになっています。手で書き足す必要はありません。
クリックしても反応しないとき
Interactivity API はエラーを出さずに静かに動かないことが多く、原因の大半は次のどれかです。ブラウザーのコンソールより先に、この4点を確認するほうが早く解決します。
名前空間が一致していない
data-wp-interactive="my-plugin/toggle-box" と store( 'my-plugin/toggle-box', ... ) の文字列は完全に一致していなければなりません。ブロック名を流用していると、my-plugin/toggle と my-plugin/toggle-box のような取り違えが起きやすいところです。一致していないと、ディレクティブが参照するアクションが見つからず何も起きません。
viewScript のまま登録している
block.json で viewScript を使っていると、import 文を含むファイルがモジュールとして読み込まれず動きません。viewScriptModule に書き換えてください。あわせて、ビルドに使っている @wordpress/scripts が古いと viewScriptModule を認識せずファイルが出力されないことがあります。build の出力先に view.js ができているかを確認し、無ければパッケージのバージョンを上げます。
apiVersion と supports の指定漏れ
apiVersion が 2 のままだったり、supports.interactivity が無いと、ディレクティブがサーバー側で処理されません。この場合は出力された HTML に data-wp- の属性がそのまま残っているので、ブラウザーの要素の検証で確認できます。属性が残っているのに動かない、というときはまずここを疑います。
アクションを呼び出しの形で書いている
data-wp-on--click="actions.toggle()" のようにかっこを付けると動きません。ディレクティブの値は JavaScript の式ではなく、ストアの中の場所を指すパスとして解釈されるためです。actions.toggle と参照だけを書きます。引数を渡したいときは、その要素の data-wp-context に値を入れてアクション側で getContext() から読む、という形にします。前述のタブの例で tab をコンテキストに入れていたのはこのためです。
まとめ
Interactivity API を使うと、フロントの動きを「ディレクティブ付きの HTML」と「状態とアクションだけのストア」に分けて書けます。準備は block.json に apiVersion: 3・supports.interactivity・viewScriptModule の3点を書くこと、実装はインスタンスごとの値を data-wp-context に、共通の値やラベルを wp_interactivity_state() で state に置くことが基本形です。表示の切り替えは data-wp-bind と data-wp-class、操作は data-wp-on、計算が必要な値は state の getter という組み合わせで、開閉やタブ切り替えといったよくある UI は DOM 操作を書かずに組めます。動かないときは名前空間の一致と viewScriptModule の指定から確認してみてください。