これまでこのブログの投稿では、さまざまな角度から WordPress ブロック開発を探ってきました。静的ブロック動的ブロックの両方の開発を検討し、コアブロックの機能拡張も行ってきました。しかし、これまで採用してきたアプローチでは、基本的にユーザーの操作にリアルタイムで反応しない標準的なブロックを作成することしかできませんでした。つまり、これらのブロックはインタラクティブではなかったのです。

この記事では、新しい強力な WordPress API「WordPress Interactivity API」を活用して、インタラクティブなブロックを作成できる新しいブロック開発のアプローチを紹介します。WordPress 6.5で導入されたこの API により、ユーザーの操作にリアルタイムで反応するブロックを作成できるようになり、リッチなユーザー体験を提供し、サイトをより魅力的で動的かつエンゲージングなものにすることができます。

Interactivity APIを使用する際の準備

Interactivity APIはReactを基盤としているため、サーバーサイドJavaScriptとReactの基本的な知識に加え、npmやnpxなどのビルドツールの知識が少なくとも必要です。また、WordPress開発およびGutenbergブロックエディターに関する十分な理解も求められます。

必要なスキルを習得したら、WordPressサイトをすばやく簡単に立ち上げられるローカル開発環境が必要になります。Kinstaでは、WordPress専用に設計したローカル開発ツール、DevKinstaを無料提供しています。DevKinstaでは、数回のクリックでローカルWordPressサイトを立ち上げ、細かくカスタマイズすることができます。

新規WordPressサイトの作成時には、以下を設定可能です。

  • トップレベルドメイン(デフォルトの.local)
  • PHPバージョン
  • データベース名
  • HTTPSの使用
  • WordPressの詳細
  • WordPressの自動更新
  • マルチサイト

Kinstaをご利用の場合は、MyKinstaで管理しているサイトをバックアップからインポートすることもできます。

DevKinstaでローカルサイトを作成
DevKinstaでローカルサイトを作成

Interactivity APIとは?

Interactivity APIはWordPressの標準APIで、Gutenbergブロック、ひいてはWordPressサイトの投稿や固定ページに「インタラクティブ性」をもたらすことができます。これは、ユーザーインタラクションを管理するための宣言的なアプローチをとる、軽量で最新のソリューションです。

インタラクティブなブロックをゼロから作成するには、高度なPHPとサーバーサイドJavaScriptの開発技術が必要になりますが、WordPressにはインタラクティブなブロックを作成するためのテンプレートが用意されているため、新たなプロジェクトごとに開発を行う必要はありません。

npx @wordpress/create-block --template @wordpress/create-block-interactive-template

このテンプレートには、インタラクティブなブロックを作成するために必要なものがすべて揃っており、最初のプロジェクトのリファレンスとして使える実例も2つ含まれています。

まずは、お好きなコマンドラインツールを開き、ローカルWordPressサイトのPluginsディレクトリに移動して、以下を貼り付けます。

npx @wordpress/create-block your-interactive-block --template @wordpress/create-block-interactive-template

インストールが完了するのを待ち、任意のコードエディターでプロジェクトフォルダを開きます。特にこだわりがなければ、Visual Studio Codeがおすすめです。

@wordpress/create-block-interactive-templateによるインタラクティブブロックのプロジェクト
@wordpress/create-block-interactive-templateによるインタラクティブブロックのプロジェクト

コマンドラインから、新しいプラグインのフォルダに移動し、以下のコマンドで開発サーバーを起動します。

npm start

これ以降、ブロックに加えた変更はリアルタイムでWordPressに反映されます。

続いてWordPress管理画面で「プラグイン」に移動し、先ほど作成したInteractivity APIプラグインを有効化します。新規投稿または固定ページを作成し、ブロックインサータで「Your interactive block(インタラクティブブロック)」を検索してコンテンツに追加します。投稿を保存し、フロントエンドでプレビューすると、以下のように2つのボタンを含む黄色いブロックが表示されます。「Switch to…」はブロックの背景色を変更し、「Toggle」は段落の内容を表示または非表示にします。

@wordpress/create-block-interactive-templateによるインタラクティブブロックの例
@wordpress/create-block-interactive-templateによるインタラクティブブロックの例

これでインタラクティブブロックを追加するためのプラグインが完成したため、続いてはインタラクティブブロックを詳しく掘り下げてみます。

インタラクティブブロックの構造

インタラクティブブロックの構造は、従来のブロックと同じです。package.jsonblock.jsonedit.jsファイル、style.scssファイルが必要になります。また加えて、サーバーサイドレンダリング用のrender.phpファイルと、フロントエンドのインタラクティブな動作を処理するためのview.jsファイルも必要です。

以下、スタータープロジェクトの個々のファイルを取り上げて、インタラクティブブロックの構造を詳しくご紹介します。

package.json

package.jsonファイルは、Nodeプロジェクトでプロジェクトの識別、スクリプトの管理、開発中の依存関係の管理とインストールに使用されます。

以下は、create-block-interactive-templateによる対話型ブロックのpackage.jsonです。

{
	"name": "your-interactive-block",
	"version": "0.1.0",
	"description": "An interactive block with the Interactivity API.",
	"author": "The WordPress Contributors",
	"license": "GPL-2.0-or-later",
	"main": "build/index.js",
	"scripts": {
		"build": "wp-scripts build --experimental-modules",
		"format": "wp-scripts format",
		"lint:css": "wp-scripts lint-style",
		"lint:js": "wp-scripts lint-js",
		"packages-update": "wp-scripts packages-update",
		"plugin-zip": "wp-scripts plugin-zip",
		"start": "wp-scripts start --experimental-modules"
	},
	"dependencies": {
		"@wordpress/interactivity": "latest"
	},
	"files": [
		"[^.]*"
	],
	"devDependencies": {
		"@wordpress/scripts": "^30.24.0"
	}
}

ここでは、scriptsdependenciesセクションが特に重要になります。

  • build:ソースコードを本番用のJavaScriptにコンパイルする。--experimental-modulesオプションはWordPressスクリプトモジュールのサポートを有効化するもの。
  • start:開発用サーバーを起動する。--experimental-modulesオプションが再度指定されていることに注意。
  • dependencies:Interactivity APIの最新パッケージのランタイム依存関係を含める。

block.json

block.jsonファイルはGutenbergブロックのマニフェスト(設定情報ファイル)です。読み込むメタデータ、メディア、スクリプト、スタイルを指定し、create-block-interactive-templateはデフォルトでは以下のblock.jsonを生成します。

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "create-block/your-interactive-block",
	"version": "0.1.0",
	"title": "Your Interactive Block",
	"category": "widgets",
	"icon": "media-interactive",
	"description": "An interactive block with the Interactivity API.",
	"example": {},
	"supports": {
		"interactivity": true
	},
	"textdomain": "your-interactive-block",
	"editorScript": "file:./index.js",
	"editorStyle": "file:./index.css",
	"style": "file:./style-index.css",
	"render": "file:./render.php",
	"viewScriptModule": "file:./view.js"
}

以下はインタラクティブブロックに必須のフィールドです。

  • apiVersion3はブロックAPIの最新バージョンで、スクリプトモジュー ルなどの最新のブロック機能に対応。
  • supports:ブロックのサポートを指定。"interactivity": trueは、 Interactivity APIのサポー トを追加する。
  • render:フロントエンドでのレンダリングを担当するPHPファイルを指定。このファイルにブロックをインタラクティブにするディレクティブを追加する。
  • viewScriptModule:インタラクティブロジックを含むJavaScriptファイルを指定。フロントエンドでのみ、そしてページがインタラクティブブロックを含む場合にのみ読み込まれる。

render.php

render.phpは動的ブロックのマークアップを構築する場所になります。ブロックをインタラクティブにするには、ブロックのDOM要素をインタラクティブにする属性を追加します。

スタータープロジェクトのrender.phpファイルは以下のとおりです。

<?php
/**
 * サーバーサイドでブロックタイプをレンダリングし、フロントエンドに表示する際に使用するPHPファイル
 *
 * このファイルで利用できる変数は次の通り
 *     $attributes (array): ブロック属性
 *     $content (string): ブロックのデフォルトコンテンツ
 *     $block (WP_Block): ブロックのインスタンス
 *
 * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render
 */

// aria-controls用の一意のIDを生成
$unique_id = wp_unique_id( 'p-' );

// グローバルステートを追加
wp_interactivity_state(
	'create-block',
	array(
		'isDark'    => false,
		'darkText'  => esc_html__( 'Switch to Light', 'your-interactive-block' ),
		'lightText' => esc_html__( 'Switch to Dark', 'your-interactive-block' ),
		'themeText'	=> esc_html__( 'Switch to Dark', 'your-interactive-block' ),
	)
);
?>

<div
	<?php echo get_block_wrapper_attributes(); ?>
	data-wp-interactive="create-block"
	<?php echo wp_interactivity_data_wp_context( array( 'isOpen' => false ) ); ?>
	data-wp-watch="callbacks.logIsOpen"
	data-wp-class--dark-theme="state.isDark"
>
	<button
		data-wp-on--click="actions.toggleTheme"
		data-wp-text="state.themeText"
	></button>

	<button
		data-wp-on--click="actions.toggleOpen"
		data-wp-bind--aria-expanded="context.isOpen"
		aria-controls="<?php echo esc_attr( $unique_id ); ?>"
	>
		<?php esc_html_e( 'Toggle', 'your-interactive-block' ); ?>
	</button>

	<p
		id="<?php echo esc_attr( $unique_id ); ?>"
		data-wp-bind--hidden="!context.isOpen"
	>
		<?php
			esc_html_e( 'Your Interactive Block - hello from an interactive block!', 'your-interactive-block' );
		?>
	</p>
</div>

それぞれの役割は、以下のようになっています。

  • wp_interactivity_state:Interactivity APIストアの初期グローバルステートを取得または設定。
  • data-wp-interactive:DOM要素とその子要素のInteractivity APIを有効にする。値はプラグインまたはブロックの固有の名前空間を設定。
  • wp_interactivity_data_wp_context():特定のHTMLノードとその子にローカルステートを提供するdata-wp-contextディレクティブを生成。
  • data-wp-watch:ノード作成時と、ステートやコンテキストの変更時にコールバックを実行
  • data-wp-class--dark-theme:HTML 要素にdark-theme クラスを追加または削除。
  • data-wp-on--click:クリックイベントで同期的にコードを実行
  • data-wp-text:HTML要素の内部テキストを設定
  • data-wp-bind--aria-expandedおよびdata-wp-bind--hidden:boolean値または文字列値に基づいて対応する要素にHTML属性を設定aria-expandedおよびhidden)。

view.js

このファイルでは、ステート、アクション、コールバックを含む、ブロックの動作に必要なロジックとデータを保持するストアを定義します。

以下は、スタータープロジェクトによって生成されたview.jsファイルです。

/**
 * WordPressの依存関係
 */
import { store, getContext } from '@wordpress/interactivity';

const { state } = store( 'create-block', {
	state: {
		get themeText() {
			return state.isDark ? state.darkText : state.lightText;
		},
	},
	actions: {
		toggleOpen() {
			const context = getContext();
			context.isOpen = ! context.isOpen;
		},
		toggleTheme() {
			state.isDark = ! state.isDark;
		},
	},
	callbacks: {
		logIsOpen: () => {
			const { isOpen } = getContext();
			// isOpen が変更されるたびにその値をログに出力
			console.log( `Is open: ${ isOpen }` );
		},
	},
} );
  • store:ブロックのグローバルステートとロジックの作成と登録に使用されるメイン関数。
  • getContext:イベントをトリガーしたDOM要素のローカルステート(context )にアクセスするために、アクションやコールバック内で使用される関数。
  • state:ブロックのグローバルなリアクティブデータを定義。
  • actions:ロジックを定義し、ステートを変更する関数を含む。
  • callbacks:特定のイベントやステートの変化に応じて自動的に実行される関数を含む。

このように多数の要素があり、混乱しそうですが、次のセクションを読めば全体が掴めるはずです。

続いては、Interactivity APIの主要な概念であるディレクティブ、ストア、ステート、アクション、コールバックについて見ていきます。

Interactivity APIのディレクティブ

Alpine.jsやVue.jsなどの他のフロントエンドライブラリと同様に、Interactivity APIは特別なHTML属性を使用し、ページ上のイベントへの応答、アプリケーションステートの更新、DOMの操作、CSSスタイルの適用、ユーザー入力の処理などを可能にします。

これらの属性は「ディレクティブ」と呼ばれ、マークアップを基礎となるJavaScriptロジックに接続します。

以下は、よく使われるディレクティブの一覧です。

機能 ディレクティブ 説明
アクティベーション/名前空間 data-wp-interactive 要素とその子要素のAPIを有効にする。値にはプラグインの一意な識別子を設定。
ローカルステート data-wp-context 現在の要素とそのすべての子要素のローカルステート(コンテキスト)を提供。JSONオブジェクトを受け取ります。PHPでこれを設定するには、wp_interactivity_data_wp_context()を使用することが推奨される(通常はrender.php)。
属性バインディング data-wp-bind--[attribute] リアクティブなステートやコンテキストの値(boolean値や文字列値)に基づいて、HTML属性を設定(disabledvalueなど)。
テキストの変更 data-wp-text 要素の内側のテキスト内容を設定。文字列のみを受け付ける。
CSSクラスの切り替え data-wp-class--[classname] boolean値によってCSSクラスを追加または削除。
インラインスタイル data-wp-style--[css-property] boolean値によってインラインスタイルクラスを追加または削除。
イベント処理 data-wp-on--[event] clickmouseoverなどの標準DOMイベントに応答してコードを実行。
初期実行 data-wp-init ノードの作成時にコールバック関数を一度だけ実行。
ステート監視 data-wp-watch ノードの作成時にコールバックを実行し、ステートやコンテキストが変更されるたびにコールバックを実行。
リストの反復処理 data-wp-each 要素のリストをレンダリング。

ディレクティブの完全なリストは、Interactivity APIの開発ノートAPIリファレンスをご覧ください。

グローバルステート、ローカルコンテキスト、派生ステート

Interactivity APIを使い始めるには、まずフロントエンド開発におけるステート管理の基本的な概念に慣れておくことが重要です。React、Vue、Angularを使って日常的に開発を行っている場合は問題ありませんが、これらの技術をこれから初めて使用する人のために、以下、一般的な定義をいくつかご紹介します。

グローバルステート

グローバルステートとは、アプリケーションのほぼすべてのコンポーネントからアクセス可能なデータセットを意味します。例えば、Interactivity APIの場合、グローバルステートはページ上のすべてのインタラクティブブロックに影響し、同期を保ちます。例えば、ユーザーが商品を買い物カゴに追加すると、買い物カゴブロックに反映されます。

Interactivity APIを使用する場合、wp_interactivity_state()を使用して、グローバルステートの初期値をサーバーに設定する必要があります。前述のスタータープロジェクトでは、この関数をrender.phpファイルで次のように使用しています。

// グローバルステートを追加
wp_interactivity_state(
	'create-block',
	array(
		'isDark'    => false,
		'darkText'  => esc_html__( 'Switch to Light', 'your-interactive-block' ),
		'lightText' => esc_html__( 'Switch to Dark', 'your-interactive-block' ),
		'themeText'	=> esc_html__( 'Switch to Dark', 'your-interactive-block' ),
	)
);

この関数は2つの引数を受け取ります。

  • ストア名前空間の一意な識別子(この場合はcreate-block
  • 既存のストア名前空間が存在する場合は、その名前空間とマージされるデータの配列

その後、初期グローバルステート値がページのレンダリングに使用されます。以下のコードのように、ディレクティブ属性値にstateを使用することで、グローバルステートの値に直接アクセスできます。

<button
	data-wp-on--click="actions.toggleTheme"
	data-wp-text="state.themeText"
></button>

store()は、選択した名前空間に限定して、JavaScriptからグローバルステートへの主なアクセスポイントを提供します。スタータープロジェクトでは、store()view.jsファイルで以下のように使われています。

import { store, getContext } from '@wordpress/interactivity';

const { state } = store( 'create-block', {
	state: { ... },
	actions: { ... },
	callbacks: { ... },
} );

グローバルステートにアクセスするには、stateプロパティを使用します。

actions: {
	toggleTheme() {
		state.isDark = ! state.isDark;
	},
},

ローカルコンテキスト

ローカルコンテキストとは、特定のコンポーネントとその直接の子からのみアクセスできるデータです。WordPressのインタラクティブブロックは、ブロックとそのネストした要素に独立した状態を提供します。

Interactivity APIを使用する場合は、getContext()を使ってローカルコンテキストにアクセスします。スタータープロジェクトの場合、ユーザーが「Toggle」ボタンをクリックすると、toggleOpen()アクションがトリガーされ、コンポーネントのローカルコンテキストにアクセスします。

actions: {
	toggleOpen() {
		const context = getContext();
		context.isOpen = ! context.isOpen;
	},
},
  • getContext():ブロックのローカルステートオブジェクトを取得。このオブジェクトのプロパティは、wp_interactivity_data_wp_context()を使用してコンポーネントマークアップ(render.php)で定義される。
  • context.isOpen = ! context.isOpen;:コンポーネントのローカルコンテキストのisOpenプロパティの値を切り替える。

派生ステート

派生ステートとは、既存のグローバルステートまたはローカルステートから動的に計算されたデータです。

以下、view.jsファイルのコード例を見てみます。

const { state } = store( 'create-block', {
	state: {
		get themeText() {
			return state.isDark ? state.darkText : state.lightText;
		},
	},
	...
}

このブロックは、create-block名前空間で定義されたグローバルステート内のthemeText派生ステートを定義します。

  • get themeText()は固定値ではなく、themeTextプロパティを読もうとするたびに実行される関数。Interactivity APIはこれをステートプロパティとして扱い、他のステートプロパティの値が変更されるたびにその値を自動的に再計算するため、通常の関数のように呼び出すことは推奨されない。上記のコードでは、isDarkプロパティの値が変更されるたび、themeTextプロパティの値が再計算される。state.isDarktrueの場合、themeTextstate.darkTextの値を取り、そうでない場合はstate.lightTextの値を取る。

このセクションでご説明した概念のより包括的な概要はこちらをご覧ください。

アクションとコールバック

アクションとコールバックは、ユーザーとの対話やステートの変化に対する応答を決定します。

インタラクティブブロックのactionsセクションには、ユーザーが生成したイベントに応答して実行される関数が含まれています。これらの関数は、主にコンポーネントのローカルまたはグローバルのステートを変更します。以下は、view.jsファイル内のコード例です。

actions: {
	toggleOpen() {
		const context = getContext();
		context.isOpen = ! context.isOpen;
	},
	...
},
  • このコードのセクションでは、toggleOpen()getContext()を使ってアクションをトリガーしたブロックのローカルコンテキストにアクセスし、isOpenプロパティの値を切り替えている。

同様に、グローバルステートにもアクセスできます。

actions: {
	...,
	toggleTheme() {
		state.isDark = ! state.isDark;
	},
},
  • toggleTheme()は、グローバルなstateオブジェクトに直接アクセスし、isDarkプロパティの値を変更。

アクションはdata-wp-on--[event]ディレクティブを介してトリガーされます。例えば、render.phpファイルに以下のボタンがあります。

<button
	data-wp-on--click="actions.toggleOpen"
	data-wp-bind--aria-expanded="context.isOpen"
	aria-controls="<?php echo esc_attr( $unique_id ); ?>"
>
  • このHTMLコードでは、data-wp-on--click属性は、ユーザーが「Toggle」ボタンをクリックすると、toggleOpenアクションを有効にする。

callbacksセクションには、依存するデータが変更されると自動的に実行される関数が含まれています。これらの関数は、ステートの変化に応答して副作用を生成することが目的です。

create-block-interactive-templateによるプロジェクトには、以下のようなコールバックがあります。

callbacks: {
	logIsOpen: () => {
		const { isOpen } = getContext();
		// isOpen が変更されるたびにその値をログに出力
		console.log( `Is open: ${ isOpen }` );
	},
},
  • logIsOpen関数は、ローカルコンテキストで使用可能な変数isOpenを使用する。
  • コールバックはgetContext()を使用してisOpenの値を取得。
  • isOpenの値が変わるたびに、関数はブラウザコンソールにメッセージを投げる。
コンソールのメッセージは、ローカルコンテキストの変更をユーザーに通知
コンソールのメッセージは、ローカルコンテキストの変更をユーザーに通知

インタラクティブブロックの作り方

理論的な部分を押さえたら、次は実践。ここからはユーザーが買い物カゴに商品を追加し、数量と合計が自動的に更新されるインタラクティブなブロックの作り方をご紹介します。この例を通して、ステート、アクション、コールバックの使い方をより明確に理解しましょう。

エディター内のインタラクティブブロック
エディター内のインタラクティブブロック

create-block-interactive-templateを使って、「Interactive Counter」ブロックを作ります。まずはコマンドラインツールを開いて、以下を貼り付けます。

npx @wordpress/create-block interactive-counter --template @wordpress/create-block-interactive-template

次に、新規プロジェクトのディレクトリに移動し、最初のビルドを実行します。

cd interactive-counter && npm run build

コードエディターでプロジェクトを開き、/srcディレクトリでblock.jsonファイルを見つけます。以下のようになっているはずです。

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "create-block/interactive-counter",
	"version": "0.1.0",
	"title": "Interactive Counter",
	"category": "widgets",
	"icon": "media-interactive",
	"description": "An interactive block with the Interactivity API.",
	"supports": {
		"interactivity": true
	},
	"textdomain": "interactive-counter",
	"editorScript": "file:./index.js",
	"editorStyle": "file:./index.css",
	"style": "file:./style-index.css",
	"render": "file:./render.php",
	"viewScriptModule": "file:./view.js"
}

これは自由にカスタマイズ可能ですが、上記の必須フィールドには変更を加えないでください。

edit.jsファイル

次に、エディターに表示されるブロックを作成します。これには、/src/edit.jsファイルを編集する必要があります。ファイルを開いて以下のように修正してください。

import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import './editor.scss';

export default function Edit({ attributes, setAttributes }) {
	const blockProps = useBlockProps();
	const products = [
		{ id: 'product1', name: __('Product 1', 'interactive-counter'), price: 10.00 },
		{ id: 'product2', name: __('Product 2', 'interactive-counter'), price: 15.00 },
		{ id: 'product3', name: __('Product 3', 'interactive-counter'), price: 20.00 },
	];

	return (
		<div {...blockProps}>
			<h3>{__('Shopping Cart', 'interactive-counter')}</h3>
			<ul>
				{products.map((product) => (
					<li key={product.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
						<span style={{ flex: 1 }}>{product.name} - ${product.price.toFixed(2)}</span>
						<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
							<button disabled>-</button>
							<span>0</span>
							<button disabled>+</button>
						</div>
						<span style={{ flex: 1, textAlign: 'right' }}>
							{__('Subtotal:', 'interactive-counter')} $0.00
						</span>
					</li>
				))}
			</ul>
			<div style={{ borderTop: '1px solid #ccc', paddingTop: '15px' }}>
				<p style={{ display: 'flex', justifyContent: 'space-between' }}>
					<strong>{__('Subtotal:', 'interactive-counter')}</strong>
					<span>$0.00</span>
				</p>
				<p style={{ display: 'flex', justifyContent: 'space-between' }}>
					<strong>{__('Tax (22%):', 'interactive-counter')}</strong>
					<span>$0.00</span>
				</p>
				<p style={{ display: 'flex', justifyContent: 'space-between' }}>
					<strong>{__('Total:', 'interactive-counter')}</strong>
					<span>$0.00</span>
				</p>
			</div>
			<p>{__('数量と合計は、フロントエンドでインタラクティブに', 'interactive-counter')}</p>
		</div>
	);
}

このコードはバックエンドにカスタムブロックを生成し、このブロックはフロントエンドでのみインタラクティブに動作します。/src/edit.jsファイルの詳細については、Gutenbergブロック開発の解説記事をご覧ください。

render.phpファイル

次に/src/render.phpファイルを編集します。ファイルを開き、既存のコードを以下のように置き換えます。

<?php
/**
 * Render callback for the interactive-counter block.
 */

$products = [
	['id' => 'product1', 'name' => __('Product 1', 'interactive-counter'), 'price' => 10.00],
	['id' => 'product2', 'name' => __('Product 2', 'interactive-counter'), 'price' => 15.00],
	['id' => 'product3', 'name' => __('Product 3', 'interactive-counter'), 'price' => 20.00],
];

// Initialize global state
wp_interactivity_state('interactive-counter', [
	'products' => array_map(function ($product) {
		return [
			'id' => $product['id'],
			'name' => $product['name'],
			'price' => $product['price'],
			'quantity' => 0,
			'subtotal' => '0.00',
		];
	}, $products),
	'vatRate' => 0.22,
]);

このコードは以下を実行します。

  1. ハードコーディングされた商品の配列を作成。各商品はID、商品名、価格を持つ。
  2. グローバルステートをwp_interactivity_stateで初期化。最初のパラメータはストア名で、view.jsで使用されているものと一致する必要がある。
  3. 以前の商品の配列をproducts配列にマップし、元の配列のプロパティに数量と小計を追加。この新しい配列は、view.jsで使用するデータ構造を提供する。
  4. vatRateで税金計算の既定値を設定。

次に、上記のコードに以下を追加します。

<div <?php echo get_block_wrapper_attributes(); ?> data-wp-interactive="interactive-counter" data-wp-init="callbacks.init">
	<h3><?php echo esc_html__('Cart', 'interactive-counter'); ?></h3>
	<ul>
		<?php foreach ($products as $index => $product) : ?>
			<li data-wp-context='{
				"productId": "<?php echo esc_attr($product['id']); ?>",
				"quantity": 0,
				"subtotal": "0.00"
			}' 
			data-wp-bind--data-wp-context.quantity="state.products[<?php echo $index; ?>].quantity" 
			data-wp-bind--data-wp-context.subtotal="state.products[<?php echo $index; ?>].subtotal">
				<span class="product-name"><?php echo esc_html($product['name']); ?> - $<?php echo esc_html(number_format($product['price'], 2)); ?></span>
				<div class="quantity-controls">
					<button data-wp-on--click="actions.decrement">-</button>
					<span data-wp-text="context.quantity">0</span>
					<button data-wp-on--click="actions.increment">+</button>
				</div>
				<span class="product-subtotal">
					<?php echo esc_html__('Subtotal:', 'interactive-counter'); ?>
					$<span data-wp-text="context.subtotal">0.00</span>
				</span>
			</li>
		<?php endforeach; ?>
	</ul>
	<div class="totals">
		<p>
			<strong><?php echo esc_html__('Subtotal:', 'interactive-counter'); ?></strong>
			$ <span data-wp-text="state.subtotal">0.00</span>
		</p>
		<p>
			<strong><?php echo esc_html__('Tax (22%):', 'interactive-counter'); ?></strong>
			$ <span data-wp-text="state.vat">0.00</span>
		</p>
		<p>
			<strong><?php echo esc_html__('Total:', 'interactive-counter'); ?></strong>
			$ <span data-wp-text="state.total">0.00</span>
		</p>
	</div>
</div>

このコードの実行内容は以下の通りです。

  • divコンテナ内のget_block_wrapper_attributes()は、ブロックの標準属性を生成するWordPress関数。この場合は、"wp-block-create-block-interactive-counter"クラス属性を生成。
  • data-wp-interactive属性でこのブロックをインタラクティブにする。
  • data-wp-init属性でview.jsで定義されたinitコールバックをトリガーする。
  • foreachループで、products配列内の各商品に対してリスト項目を生成。
  • data-wp-contextブロックのローカルコンテキストを定義。
  • data-wp-binddata-wp-context.quantityの値をグローバルステートのproducts[$index].quantity プロパティに結びつける。

  • 同様のことが以下の小計の行でも実行される。
  • 次の2つのボタンはdata-wp-on--click属性により、decrementincrementアクションを有効化。
  • spandata-wp-text属性は、context.quantityの現在の値に基づいて要素の内容を更新。

残りのコードは説明するまでもないため、次のファイルに進みましょう。

view.jsファイル

このファイルには、インタラクティブブロックのロジックが含まれています。

import { store, getContext } from '@wordpress/interactivity';

store('interactive-counter', {
	state: {
		get subtotal() {
			const { products } = store('interactive-counter').state;
			return products
				.reduce((sum, product) => sum + product.price * (product.quantity || 0), 0)
				.toFixed(2);
		},
		get vat() {
			const { subtotal, vatRate } = store('interactive-counter').state;
			return (subtotal * vatRate).toFixed(2);
		},
		get total() {
			const { subtotal, vat } = store('interactive-counter').state;
			return (parseFloat(subtotal) + parseFloat(vat)).toFixed(2);
		},
	},
	actions: {
		increment: () => {
			const context = getContext();
			const { products } = store('interactive-counter').state;
			const product = products.find(p => p.id === context.productId);
			if (product) {
				product.quantity = (product.quantity || 0) + 1;
				product.subtotal = (product.price * product.quantity).toFixed(2);
				context.quantity = product.quantity;
				context.subtotal = product.subtotal;
				console.log(`Incremented ${context.productId}:`, { quantity: product.quantity, subtotal: product.subtotal, context });
			} else {
				console.warn('Product not found:', context.productId);
			}
		},
		decrement: () => {
			const context = getContext();
			const { products } = store('interactive-counter').state;
			const product = products.find(p => p.id === context.productId);
			if (product && (product.quantity || 0) > 0) {
				product.quantity -= 1;
				product.subtotal = (product.price * product.quantity).toFixed(2);
				context.quantity = product.quantity;
				context.subtotal = product.subtotal;
				console.log(`Decremented ${context.productId}:`, { quantity: product.quantity, subtotal: product.subtotal, context });
			} else {
				console.warn('Cannot decrement:', context.productId, product?.quantity);
			}
		},
	},
	callbacks: {
		init: () => {
			const { products } = store('interactive-counter').state;
			products.forEach((product, index) => {
				product.quantity = 0;
				product.subtotal = '0.00';
				console.log(`Initialized product ${index}:`, { id: product.id, quantity: product.quantity, subtotal: product.subtotal });
			});
		},
	},
});

このファイルは、interactive-counter名前空間のストアを定義します。以下でステート、アクション、コールバックを管理します。

store('interactive-counter', {
	state: { ... },
	actions: { ... },
	callbacks: { ... },
});
  • statesubtotalvat、およびtotalの3つの計算状態プロパティ(ゲッター)を定義。これらの関数は、グローバルステートから値を取得し、返される値を計算する。
  • actions:イベント時に実行される2つの関数、incrementおよびdecrementを定義。これらの関数は、グローバルステートからproducts配列を取得し、context.productIdに基づいてローカルコンテキストから現在の商品を取得し、現在の商品のプロパティ値(quantitysubtotal)を更新し、新しい値でローカルコンテキストを同期する。
  • callbacks:初期化のためのinitコールバックを定義。

以下は、インタラクティブブロックのフロントエンドでの見え方です。

Interactivity APIで作成したインタラクティブカウンター
Interactivity APIで作成したインタラクティブカウンター

まとめ

今回は、WordPressのInteractivity APIの主な機能をご紹介しました。グローバルステート、ローカルコンテキスト、ディレクティブ、アクション、コールバックなどの主要な概念を掘り下げ、@wordpress/create-block-interactive-templateを使ってゼロからインタラクティブブロックを作成する方法、ユーザーの入力と連動するブロックを実際に作成する方法を解説しています。

WordPress Interactivity APIを使って、動的でインタラクティブなWordPressサイト開発の参考になりましたら幸いです。

Carlo Daniele Kinsta

ウェブデザインとフロントエンド開発をこよなく愛し、WordPress歴は10年以上。イタリアおよびヨーロッパの大学や教育機関とも共同研究を行う。WordPressに関する記事を何十件も執筆しており、イタリア国内外のウェブサイトや雑誌に掲載されている。詳しい仕事情報はXとLinkedInで公開中。