Interactivity APIとは?
ゴリゴリaddEventListenerとかで書いてたJSがサクッと書けるようになるAPIです。(かなりざっくり)
これを使ったらブロック間のやりとりとかもできるようになるので、かなり用途が広いAPIだと思います。
今回はこれを使ってブロックを作ってみました。
ブロックつくってみた
https://github.com/chiilog/iapi-tabs
Interactivity APIを使ったタブブロックです。
実質3〜4日で作りました。Alpine.js とか触ったことある人だともっと理解が早そう。(私は触ったことなかった)
管理画面
表示
使い方
親ブロックとして使うブロックのblock.json のsupports
に "interactivity": true
を入れます。
また、view.js と render.php を使うので、"viewScriptModule": "file:./view.js"
と "render": "file:./render.php"
を追加します。
いれるとこんな感じになります。
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "chiilog-blocks/iapi-tabs",
"version": "0.1.0",
"title": "IAPI Tabs",
"category": "widgets",
"icon": "smiley",
"description": "Example block scaffolded with Create Block tool.",
"attributes": {
"contents": {
"type": "array",
"default": []
},
"tabNavText": {
"type": "string",
"source": "text",
"selector": ".wp-block-chiilog-blocks-iapi-tabs__button span"
}
},
"supports": {
"interactivity": true,
"anchor": true
},
"textdomain": "chiilog-iapi-tabs",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"viewScriptModule": "file:./view.js",
"render": "file:./render.php"
}
これでAPIを使う準備はOK!
次はブロックを用意します。
私は先にrender.php で完成形のHTMLタグを入れただけのファイルを作りました。
<?php
/**
* @var array $attributes The block attributes.
* @var string $content The block default content.
* @var WP_Block $block The block instance.
*
* @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render
*/
?>
<div
<?php echo get_block_wrapper_attributes(); ?>
>
<div class="wp-block-chiilog-blocks-iapi-tabs__nav" role="tablist">
<button
role="tab"
class="wp-block-chiilog-blocks-iapi-tabs__button"
aria-selected="true"
id="tab-1"
aria-controls="panel-1"
tabindex="0"
>
<span>ナビ1</span>
</button>
<button
role="tab"
class="wp-block-chiilog-blocks-iapi-tabs__button"
aria-selected="false"
id="tab-2"
aria-controls="panel-2"
tabindex="-1"
>
<span>ナビ2</span>
</button>
<button
role="tab"
class="wp-block-chiilog-blocks-iapi-tabs__button"
aria-selected="false"
id="tab-3"
aria-controls="panel-3"
tabindex="-1"
>
<span>ナビ3</span>
</button>
</div>
<div
id="panel-1"
role="tabpanel"
tabindex="0"
aria-labelledby="tab-1"
class="wp-block-chiilog-blocks-iapi-tabs__panel"
aria-expanded="true"
aria-hidden="false"
>
パネル1
</div>
<div
id="panel-2"
role="tabpanel"
tabindex="0"
aria-labelledby="tab-2"
class="wp-block-chiilog-blocks-iapi-tabs__panel"
aria-expanded="false"
aria-hidden="true"
>
パネル2
</div>
<div
id="panel-3"
role="tabpanel"
tabindex="0"
aria-labelledby="tab-3"
class="wp-block-chiilog-blocks-iapi-tabs__panel"
aria-expanded="false"
aria-hidden="true"
>
パネル3
</div>
</div>
ただ動かしてみるだけならedit.js もこれベタっと貼っておくだけでもいいですね。私はAPI触るために作ってたのに、なぜかeditの中身も作り込んでいました。(夫に突っ込まれてedit別に作り込まなくてよかったことに気づいた)
ちなみに今回はeditの中身については省きます。
次はこのタグにAPI用のタグを仕込んでいきます。
親のdivにdata-wp-interactive="chiilog-iapi-tabs"
を入れます。これの内部でbindなりclickなりいろんなJSが動きます。
<div
<?php echo get_block_wrapper_attributes(); ?>
data-wp-interactive="chiilog-iapi-tabs"
>
...
</div>
こんな感じ。
このdata-wp-interactive
に指定したchiilog-iapi-tabs
がストア名になります。
view.jsに以下のように書きます。
/**
* WordPress dependencies
*/
import { store } from '@wordpress/interactivity';
store( 'chiilog-iapi-tabs', {
state: {},
actions: {},
callbacks: {},
} );
例えば以下のようにactionsの中に書いて、ボタンにclickの動作をつけるとコンソールにclickが出ます。
/**
* WordPress dependencies
*/
import { store } from '@wordpress/interactivity';
store( 'chiilog-iapi-tabs', {
state: {},
actions: {
selectTab: () => {
console.log( 'click' );
},
},
callbacks: {},
} );
<button
role="tab"
class="tab-nav__button"
data-wp-on--click="actions.selectTab"
aria-selected="true"
id="tab-1"
aria-controls="panel-1"
tabindex="0"
>
<span>ナビ1</span>
</button>
無事clickが動作していることを確認したので、ベタ書きしていた記述をブロックに置き換えます。
<?php
/**
* @var array $attributes The block attributes.
* @var string $content The block content.
* @var WP_Block $block The block object.
*
* @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render
*/
$navItems = $attributes['contents'];
?>
<div
<?php echo get_block_wrapper_attributes(); ?>
data-wp-interactive="chiilog-iapi-tabs"
>
<div class="wp-block-chiilog-blocks-iapi-tabs__nav" role="tablist">
<?php
if ( $navItems ) :
foreach ( $navItems as $index => $navItem ) :
$tabNumber = $index + 1;
?>
<button
role="tab"
class="wp-block-chiilog-blocks-iapi-tabs__button"
data-wp-on--click="actions.selectTab"
aria-selected="<?php echo esc_attr( $index === 0 ? 'true' : 'false' ); ?>"
id="tab-<?php echo esc_attr( $tabNumber ); ?>"
aria-controls="panel-<?php echo esc_attr( $tabNumber ); ?>"
tabindex="<?php echo esc_attr( $index === 0 ? '0' : '-1' ); ?>"
>
<?php echo esc_html( $navItem['tabNavText'] ); ?>
</button>
<?php
endforeach;
endif;
?>
</div>
<div class="wp-block-chiilog-blocks-iapi-tabs__panels">
<?php echo do_blocks( $content ); ?>
</div>
</div>
この時点ではaria-selectedとかはまだほぼベタ打ちのままです。
パネル部分はインナーブロックの中にパネルブロック(作成したブロック。インナーブロックはこのパネルブロックしか配置できないように設定)を置く形にしています。
おおよその形にはなったので、タブのJSを書いていきます。
いつものJSならクリックしたナビ以外のaria-selectedを変えたりtabindexを変えたりして実装してるのですが、今回はInteractivity APIを使って実装していくので、
- state.currentTab でカレントのタブを管理
- ボタンそれぞれに
data-wp-context='{ "position":
(number)}'
をつけてタブの番号を付与 - state.currentTabとcontext.positionが同一であればカレントにする
という処理をしました。
state.currentTabはwp_interactivity_state
で管理するため、render.phpに追加します。
wp_interactivity_state( 'chiilog-iapi-tabs', array (
'currentTab' => 0
));
ボタンにもAPIのディレクティブをつけていきます。
<button
role="tab"
class="wp-block-chiilog-blocks-iapi-tabs__button"
data-wp-on--click="actions.changeCurrentTab"
data-wp-bind--aria-selected="state.tabSelected"
id="tab-<?php echo esc_attr( $tabNumber ); ?>"
aria-controls="panel-<?php echo esc_attr( $tabNumber ); ?>"
data-wp-bind--tabindex="state.tabIndex"
data-wp-context='{ "position": <?php echo esc_attr( $index ); ?> }'
>
<?php echo esc_html( $navItem['tabNavText'] ); ?>
</button>
さて、次はパネルです。パネルはインナーブロックで管理しているので、ボタンのようにrender.php内で処理することはできません。
なので、パネルブロックにディレクティブをつけるためにrender_block
のフィルターフックを使います。
今回は単一のブロックのみでの動作なので、render_block_{$this->name}
のフィルターを使いました。
function add_directives_to_inner_blocks( $block_content, $block ) {
$panels = new WP_HTML_Tag_Processor( $block_content );
$panelCount = 0;
while ( $panels->next_tag() ) {
foreach ( $panels->class_list() as $class_name ) {
if ( $class_name === 'wp-block-chiilog-blocks-iapi-tabs-panel' ) {
$panels->set_attribute( 'data-wp-bind--aria-expanded', 'state.panelExpanded' );
$panels->set_attribute( 'data-wp-bind--aria-hidden', 'state.panelHidden' );
$panels->set_attribute( 'data-wp-context', '{ "position": ' . $panelCount . ' }' );
$panelCount++;
}
}
}
return $panels->get_updated_html();
}
add_filter( 'render_block_chiilog-blocks/iapi-tabs', 'add_directives_to_inner_blocks', 10, 2 );
ブロック間でデータのやりとりをしたいときはよく使うことになるんだろうなと思います。
WP_HTML_Tag_Processorはブロック作るに限らず色々使い道があるので(aタグをspanに変えたいとか)覚えておくとよさそう。
これで描写側のセットは完了したので、あとはview.jsでJSをゴリゴリしていくだけです。
/**
* WordPress dependencies
*/
import { getContext, store } from '@wordpress/interactivity';
const { state, actions } = store( 'chiilog-iapi-tabs', {
state: {
get panelExpanded() {
const ctx = getContext();
return ctx.position === state.currentTab;
},
get panelHidden() {
const ctx = getContext();
return ctx.position !== state.currentTab;
},
get tabSelected() {
const ctx = getContext();
return ctx.position === state.currentTab;
},
get tabIndex() {
const ctx = getContext();
return ctx.position === state.currentTab ? 0 : -1;
},
},
actions: {
changeCurrentTab: () => {
const ctx = getContext();
state.currentTab = ctx.position;
},
},
callbacks: {},
} );
なんということでしょう。書いたのはこれだけ!タブみたいなシンプルなやつだったからというのはあるけど、めちゃくちゃ簡単に動作追加ができました。
ここで終わったと思った?残念!まだ続きます
ふと翌日に複数配置したらどうなるんだ?って配置してみたら、なんとまあ連動して全部動く!
これが例えば決済ボタンだとか、ページで1つしか配置できないようにしているものならsupportsでmultiple: false
にしておけばいいけど、これはタブブロック。複数配置することも考えられるもの。あと、ついでにボタンとかパネルについてるidもユニークにしておかねばならぬ。
というわけで、ブロックのclientIdをattributes: tabClientId
に保存して使うことにしました。(save.js でやってたらこんな回りくどいことしなくてももっと簡単だったはず)
<button
role="tab"
class="wp-block-chiilog-blocks-iapi-tabs__button"
data-wp-on--click="actions.changeCurrentTab"
data-wp-bind--aria-selected="state.tabSelected"
id="tab-<?php echo esc_attr( $tabClientId ) . '-' . esc_attr( $tabNumber ); ?>"
aria-controls="panel-<?php echo esc_attr( $tabClientId ) . '-' . esc_attr( $tabNumber ); ?>"
data-wp-bind--tabindex="state.tabIndex"
data-wp-context='{ "position": <?php echo esc_attr( $index ); ?> }'
>
<?php echo esc_html( $navItem['tabNavText'] ); ?>
</button>
肝心のブロックが全部一緒に動いちゃう部分ですが、これは
wp_interactivity_state( 'chiilog-iapi-tabs', array (
'currentTab' => 0
));
で最初にステートを定義しているのが問題でした。なので、このカレントの管理をブロック自身のコンテキストに持たせるようにしました。
<?php
/**
* @var array $attributes The block attributes.
* @var string $content The block content.
* @var WP_Block $block The block object.
*
* @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render
*/
$navItems = $attributes['contents'];
$tabClientId = $attributes['tabClientId'];
$context = array(
'currentTab' => 0,
);
?>
<div
<?php echo get_block_wrapper_attributes(); ?>
data-wp-interactive="chiilog-iapi-tabs"
data-tab-client-id="<?php echo esc_attr( $tabClientId ); ?>"
<?php echo wp_interactivity_data_wp_context( $context ); ?>
>
...
</div>
view.js でもステートを参照していたので、changeCurrentTab をstate.currentTabから context.currentTabに差し替えます。
/**
* WordPress dependencies
*/
import { getContext, store } from '@wordpress/interactivity';
const { state, actions } = store( `chiilog-iapi-tabs`, {
state: {
get panelExpanded() {
const ctx = getContext();
return ctx.position === ctx.currentTab;
},
get panelHidden() {
const ctx = getContext();
return ctx.position !== ctx.currentTab;
},
get tabSelected() {
const ctx = getContext();
return ctx.position === ctx.currentTab;
},
get tabIndex() {
const ctx = getContext();
return ctx.position === ctx.currentTab ? 0 : -1;
},
},
actions: {
changeCurrentTab: () => {
const ctx = getContext();
ctx.currentTab = ctx.position;
},
},
callbacks: {},
} );
これでどれだけタブを置いても一緒にカレントが動いてしまうことはなくなりました!
作ってみての感想
最初は難しそうだなーできるかなーと思ってたんですが、案外サクッとできてしまいました。組み合わせてswiperと連動させたりとかもできるのかな…どきどき。
とは言え、書くのにはJSの知識が必要なので、しっかりJS勉強しておかないといけないなあという感じです。まだまだ知識不足感は否めません。
あと、実装するうえで結構ChatGPTに助けてもらいました。
エラー文の解説もそうですが、コードをベタっと貼り付けて「ここがこうなってるんだけどどうして?」って聞くとわりといい返事を返してくれたので、問題の解消のヒント(ときには答え)にかなり役立ちました。頼れる相棒です。答えに「それはホントか?」って思ったら根拠を調べたりするので勉強にもなります。
ちなみに、ChatGPT自体にInteractivity API自体の知識はまだないのでこの辺は聞いてません。
実装中のメモはzennでまとめています。
https://zenn.dev/chiilog/scraps/1129dfe7a551d7
参考にしたサイト、GitHub等
Interactivity APIとはなんぞや?でまずは触ったCookbookのレシピ
上記をもとに作られたより強力なスライダーのリポジトリ。WP_HTML_Tag_Processorまわり等はこちらを参考にしました。
https://github.com/ryanwelcher/iapi-gallery-slider
タブのeditを実装するにあたり、キタジマさんのSnow Monkey Blocksのタブブロックをめちゃくちゃ参考にさせてもらいました。
https://github.com/inc2734/snow-monkey-blocks/tree/master/src/blocks/tabs
ありがとうございました!