マイナスカウンターブロックを作成してInteractivity APIの挙動を確認する

シンプルでリアクティブなマイナスカウンターを作成して、WordPressのInteractivity APIの仕様・挙動を確認します。

※リアクティブ:データの変化に自動で反応して UI を更新する仕組み

雛形を生成する

Interactivity APIに対応したブロックの雛形をインストールする。

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

各ファイルの初期設定を確認する

package.json

{
	"scripts": {
		"build": "wp-scripts build --experimental-modules",
		"start": "wp-scripts start --experimental-modules"
	},
}

minus-counter.php

ブロックを登録する。

function create_block_minus_counter_block_init() {
	register_block_type_from_metadata( __DIR__ . '/build' );
}
add_action( 'init', 'create_block_minus_counter_block_init' );

block.json

Interactivity APIの有効化と、モジュールとしてview.jsをビルドする設定。

{
	"supports": {
		"interactivity": true
	},
	"viewScriptModule": "file:./view.js"
}

stateをグローバルで管理する場合

edit.js

import { useBlockProps } from "@wordpress/block-editor";

export default function Edit() {
    const blockProps = useBlockProps();

    return (
        <p {...blockProps}>
            <p className="counter-text">0</p>
            <button className="counter-button">Minus Counter</button>
        </p>
    );
}

render.php

<div
	data-wp-interactive="minus-counter"
	<?php echo get_block_wrapper_attributes(); ?>>
	<p
		data-wp-text="state.count"
		class="counter-text"></p>
	<button
		data-wp-on--click="actions.countDown"
		class="counter-button">Minus Counter</button>
</div>

data-wp-interactive=”minus-counter”
このディレクティブが書かれたHTML要素をWordPress Interactivity API の「インタラクティブな対象」として識別し、JavaScript 側と連携するための識別子。この場合、view.jsで定義したminus-counterというstoreと紐づいていることを表す。ラッパー要素にだけ指定すれば子要素に伝播する。

data-wp-text=”state.count”
WordPress Interactivity API の「テキストバインディング」機能。このHTML要素のテキスト内容を、state.count の値にリアルタイムに反映するように指示している。

view.js

/**
 * WordPress dependencies
 */
import { store } from "@wordpress/interactivity";

const { state } = store("minus-counter", {
    state: {
        count: 10,
    },
    actions: {
        countDown: () => {
            return state.count--;
        },
    },
    callbacks: {},
});

state.count
初期値は10

actions.countDown
state.countの値をマイナス1して呼び出し元に返す

このブロックの場合、stateがグローバルな状態のため、複数個ブロックを挿入すると値が連動してしまう。

stateをローカルで管理する場合

stateをローカルスコープで定義する場合、stateが独立した値として保持される。そのため、同一ブロックを複数回挿入してもstateの値が連動しない。

edit.js

import { useBlockProps } from "@wordpress/block-editor";

export default function Edit() {
    const blockProps = useBlockProps();

    return (
        <p {...blockProps}>
            <p className="counter-text">0</p>
            <button className="counter-button">Minus Counter</button>
        </p>
    );
}

変更なし。

render.php

<?php
$context = [
	'minusCount' => 10,
]
?>

<div
	data-wp-interactive="minus-counter"
	<?php echo wp_interactivity_data_wp_context($context); ?>
	<?php echo get_block_wrapper_attributes(); ?>>
	<p
		data-wp-text="state.count"
		class="counter-text"></p>
	<button
		data-wp-on--click="actions.countDown"
		class="counter-button">Minus Counter</button>
</div>

wp_interactivity_data_wp_context($context);
PHP で定義した値(配列$context)を JSON に変換して、HTML の data-wp-context 属性として出力する。HTMLの出力結果は次のようになる。

data-wp-context='{"minusCount":10}'

このdata-wp-context によって、JavaScript 側の getContext() で minusCount を参照できるようになる。

なぜこのような仕組みなのかというと、PHP と JavaScript は実行されるタイミングと環境が完全に別物のため、render.php(PHP)で定義した配列に、view.js(JavaScript)から直接アクセスできないから。PHPがHTMLソースとして書き出すことで、JavaScript への値の受け渡しができるようになっている。

また、wp_interactivity_data_wp_context( $context ) を使って data-wp-context を HTML に出力すると、Interactivity API の「ローカル state(スコープ)」として動作するようになる。なぜなら、data-wp-context があると、このブロック専用の状態オブジェクト(context)を内部的に作り、その DOM 要素を「state のローカルスコープ」として認識するため。

PHP側で $context = [ ‘minusCount’ => 10 ]; を定義して data-wp-context に出力するのは、「初期値を各ブロックごとに独立して持たせたい(=共有したくない)」から。

view.js

/**
 * WordPress dependencies
 */
import { store, getContext } from "@wordpress/interactivity";

const { state } = store("minus-counter", {
    state: {
        get count() {
            return getContext().minusCount;
        },
    },
    actions: {
        countDown: () => {
            getContext().minusCount--;
        },
    },
});

store() 関数

  • 第1引数:このストアの名前(識別子)。HTML 側の data-wp-interactiveと結びつく
  • 第2引数:ストアの中身(振る舞い) を定義。状態・アクション・コールバックなどをまとめたオブジェクト
state: {
	get count() {
		return getContext().minusCount;
	}
},
  • state は「表示に使うリアクティブな状態」を定義
  • count は getter なので、HTML 側の data-wp-text="state.count" で使える
    • get count() は、get キーワードを使って「count という名前のゲッター関数」を定義
    • 呼び出すときは関数なのに()をつけない。data-wp-text=”state.count”
    • 見た目はプロパティ(変数)だが、裏で関数が呼び出されている
  • getContext().minusCount にアクセスすることで、PHP から渡された data-wp-context='{"minusCount": 10}' を参照する
actions: {
	countDown: () => {
		getContext().minusCount--;
	}
}
  • actions は「ユーザーの操作に反応して状態を変更する関数群」
  • 例:data-wp-on--click="actions.countDown" としてボタンにバインド可能
  • この関数では getContext().minusCount-- によって状態を 1 減らす

カウントがマイナスになったら処理を停止する

view.js

const { state } = store("minus-counter", {
    state: {
        get count() {
            return getContext().minusCount;
        },
        get isDisabled() {
            return getContext().minusCount <= 0;
        },
    },
    actions: {
        countDown: () => {
            if (getContext().minusCount > 0) {
                getContext().minusCount--;
            }
        },
    },
});

render.php

<?php
$context = [
	'minusCount' => 10,
];
?>

<div
	data-wp-interactive="minus-counter"
	<?php echo wp_interactivity_data_wp_context($context); ?>
	<?php echo get_block_wrapper_attributes(); ?>>

	<p
		data-wp-text="state.count"
		class="counter-text"></p>

	<button
		data-wp-on--click="actions.countDown"
		data-wp-bind--disabled="state.isDisabled"
		class="counter-button">Minus Counter</button>
</div>

HTMLにdisabled属性を付与する処理
view.jsにて、isDisabledというゲッター関数を定義し、HTMLから取得したminusCountが0以下になった時にtrueを返す。
render.phpから生成されたHTMLにて、data-wp-bind–disabled=”state.isDisabled”のstate.isDisabledがtrueになると、disabled属性が自動的に出力される。
data-wp-bind--属性名="式" は、その式が true の場合にのみ、該当の HTML 属性が出力される。

カウントが0以下になった場合に処理を停止する
view.jsに(getContext().minusCount > 0)という条件を追加し、minusCount が0以下になった場合には、minusCountの更新は行わないためカウントダウンが停止する。