Gutenberg Componentsを使ってプラグインの設定画面を作る

はじめに

WordPressプラグインを開発する時に、機能問わず設定ページを作成するケースは多いと思います。

一般的には、add_options_pageフックなどで管理画面に独自ページを追加し、第5引数に指定したコールバック関数内でコンテンツを生成する手法がとられます。

管理メニューの追加 – WordPress Codex 日本語版

今回、カスタムブロック開発にあたって管理画面もReact(Gutenberg Components)で構築してみた所、思っていたよりシンプルに開発できたため、開発の中で得られた知見を、サンプルプラグインを開発するチュートリアル形式で紹介したいと思います。

なぜReactなのか

カスタムブロック開発でもないのに、なぜわざわざReactを設定画面に導入するのか、開発の中で感じられたメリットを挙げてみます。

コンテンツ部分をComponentとして切りだせる

従来の手法では、PHPで設定画面のコンテンツをゴリゴリ書いていた箇所を別ファイルに切りだす事ができ、コードの見通しが良くなったと感じました。

PHPでもclass化したりすることで多少分離は出来ますが、例えばタブでコンテンツを切り替えるような場合であったり、ページ内に多数の設定項目があった場合などでコード量が増えた時に、Reactであればそれぞれを適度な粒度でComponent化(別ファイル化)して、適宜importするで対処出来ます。

状態管理が簡単

例えば、「設定AがOFFの場合は設定Bは非表示にしたい」のような動的な動きを持たせたい場合、従来の手法ではJavaScriptで一つ一つイベント処理を書いていく必要がありました。

Reactであれば各設定項目の状態をstate(後述)として管理でき、stateをもとにした条件文を書くだけで実現できるので、コード量がグッと減ります。

CSSがデフォルトでいい感じに当たっている

Gutenberg Compoentは、それぞれにデフォルトでスタイルが適用されています。

例えばチェックボックスの代わりに利用出来る ToggleControl ですが、わざわざトグルボタン用のCSSを書かなくても、Componentをimport・設置するだけでいい感じに表示してくれます。

そのため、少ないCSSコードで統一感のあるUIを作成する事が出来ます。

備考

今回開発するサンプルプラグインはあくまで初歩的なものですので、不十分な点や改善点が多々あります。
ブラッシュアップするためのTipsは最終章の応用編に記載していますので、余裕がある方がいましたら試してみて下さい。

前提となる知識・技術

  • 設定画面付きのプラグインの開発
  • ES6、JSXの初歩的な知識
  • npmを使ったローカル開発環境の構築

以上の知識・技術がある事を前提として、本題から外れる内容は詳しく記載していませんのでご了承下さい。

開発するプラグインの概要

今回は設定画面の開発がメインテーマですので、以下のような至極シンプルな機能を想定します。

  • 全ページの右下に、固定レイアウトで広告を表示する
  • 広告表示の有無、テキスト、文字サイズを設定画面で変更出来る
開発するプラグインの概要

サンプルコード

今回のチュートリアルで開発したプラグインを整理したソースは、以下リポジトリで確認出来ます。

https://github.com/t-hamano/my-gutenberg-admin-plugin

STEP1:プラグインのベースの作成

プラグイン名(ディレクトリ名)は my-gutenberg-admin-plugin とします。

WordPress管理画面でプラグインとして認識させるための最低限のヘッダー情報と、あわせて今回メインで開発するオプションページを追加する処理を記述しておきます。

{WordPressインストールディレクトリ}/wp-content/plugins/my-gutenberg-admin-plugin/my-gutenberg-admin-plugin.php に、以下を記述します。

<?php
/**
 * Plugin Name: My Gutenberg Admin Plugin
 */

// オプションページの追加
function my_plugin_menu() {
    add_options_page(
        'My Gutenberg Admin Plugin', // メニューを選択した時にページのタイトルタグに表示されるテキスト
        'My Gutenberg Admin Plugin', // メニューで表示されるテキスト
        'administrator',             // メニューを使用出来る権限(今回は管理者)
        'my-gutenberg-admin-plugin', // スラッグ名(URLの一部にもなる)
        'my_settings_page'           // コールバック関数
    );
};
add_action( 'admin_menu', 'my_plugin_menu' );

// オプションページのコンテンツ
function my_settings_page() {
    echo '<div id="my-gutenberg-admin-plugin"></div>';
}

ここでのポイントは、add_options_page の第5引数に指定したcallback関数で、ID付きの空のdivタグのみを出力しているという点です。

これまでの手法では、ここに form タグ、settings_fields / do_settings_sections / wp_nonce_field関数などを使用して、ゴリゴリと設定画面を書いていたと思います。

今回は、設定画面のコンテンツは全てReact(Gutenberg Component)で生成するので、ルートDOMノードのみを出力します。

まずはここまでのステップで、WordPress管理画面でプラグインが認識されており、サイドメニューからプラグインページへアクセスすると空ページが表示される事を確認してください。

空ページ

STEP2:ビルド環境の構築・準備

設定画面の開発にあたって、ES6 + JSXを使用するためのビルド環境を構築します。

まずは、 {WordPressインストールディレクトリ}/wp-content/plugins/my-gutenberg-admin-plugin/ で、 npm init コマンドで package.json を生成します。
今回はテストなので、全てYes(Enterキー押下)で進めていきます。

ビルド環境のセットアップについては、自前で webpack などを用意してもよいですが、ブロックエディターハンドブックのチュートリアル(JavaScript ビルド環境のセットアップ)に従い、 @wordpress/scripts を使用します。

npm install --save-dev @wordpress/scripts

あわせて、 wp-scripts のコマンドを簡単に実行できるようにするために、 package.jsonscriptsstartbuild を記述しておきます。

 "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "wp-scripts start",
    "build": "wp-scripts build"
  },

続いて、エントリポイントとなるjsファイルと、管理画面に適用するscssファイルをsrcディレクトリに配置します。

@wordpress/scripts パッケージは、デフォルトで src/index.js をエントリポイントとし、ビルドファイルをbuild/ に出力します。

└── plugins
    └── my-gutenberg-admin-plugin
        ├── src
        │    ├── index.js   // ←追加
        │    └── admin.scss // ←追加
        ├── my-gutenberg-admin-plugin.php
        ├── package-lock.json
        └── package.json

また、ファイルの読み込み・ビルド確認用に適当なコードを記述しておきます。

// 設定画面用スタイル
import './admin.scss';

// JavaScriptファイル読み込み確認
console.log('admin scripts loaded.')
// add_options_pageのコールバック関数で出力したdivタグに、適当なスタイルを当ててみる
#my-gutenberg-admin-plugin {
    min-height: 300px;
    border: 3px red solid;
}

ここで一旦ビルドスクリプトを実行し、buildディレクトリにファイルが生成される事を確認します。

npm run build
└── plugins
    └── my-gutenberg-admin-plugin
        ├── build
        │   ├── index.asset.php // ←自動生成
        │   ├── index.css       // ←自動生成
        │   └── index.js        // ←自動生成
        ├── src
        │    ├── index.js
        │    └── admin.scss
        ├── my-gutenberg-admin-plugin.php
        ├── package-lock.json
        └── package.json

STEP3:オプションページへのファイル読み込み

STEP2で生成したjsファイル・cssファイルを、admin_enqueue_scriptsフックを使って、作成したオプションページのみで読み込むようにします。

以下のコードを、 my-gutenberg-admin-plugin.php に追記します。

// ~~省略~~

// オプションページのコンテンツ
function my_settings_page() {
    echo '<div id="my-gutenberg-admin-plugin"></div>';
}

function my_admin_scripts( $hook_suffix ) {

    // 作成したオプションページ以外では読み込まない
    if ( 'settings_page_my-gutenberg-admin-plugin' !== $hook_suffix ) {
        return;
    }

    // 依存スクリプト・バージョンが記述されたファイルを読み込み
    $asset_file = include( plugin_dir_path( __FILE__ ) . '/build/index.asset.php' );

    // CSSファイルの読み込み
    wp_enqueue_style(
        'my-gutenberg-admin-plugin-style',
        plugin_dir_url( __FILE__ ) . '/build/index.css',
        array( 'wp-components' ) // ←Gutenbergコンポーネントのデフォルトスタイルを読み込み
    );

    // JavaScriptファイルの読み込み
    wp_enqueue_script(
        'my-gutenberg-admin-plugin-script',
        plugin_dir_url( __FILE__ ) . '/build/index.js',
        $asset_file['dependencies'],
        $asset_file['version'],
        true // </body>`終了タグの直前でスクリプトを読み込む
    );
}
add_action( 'admin_enqueue_scripts', 'my_admin_scripts' );

カスタムブロックやプラグインを開発された事のある方にはおなじみの記述かもしれませんが、ポイントとなるのはCSSを読み込む wp_enqueue_style の第3引数に、依存スタイルとして wp-components を指定するという点です。

カスタムブロック開発でエディタ側にCSSを読み込ませる時とは違い、明示的に指定する必要があります。

もう一つのポイントは、JavaScriptを読み込む wp_enqueue_script の第5引数にtrueを指定し、</body> 終了タグの直前でスクリプトが読み込まれるようにします。
(第5引数のデフォルト値はtrue<head>タグ内で読み込まれる)

true を指定しないと、ルートDOM( <div id="my-gutenberg-admin-plugin"></div> )が生成される前にレンダリングが走ってしまいエラーとなります。

ここまでで改めてオプションページを確認してみると、ブラウザコンソールにJavaScriptの確認用コードが出力され、またコンテンツエリアのルートDOM( #my-gutenberg-admin-plugin )にスタイルが当たっている事が確認出来ます。

コンテンツエリアのルートDOM( #my-gutenberg-admin-plugin )にスタイルが当たっている

確認したら、 src/admin.scss に記述した確認用スタイルは削除しておきましょう。

#my-gutenberg-admin-plugin {
}

STEP4:オプションページのベースコード作成

いよいよ、React(Gutenberg Components)を使ってオプションページにコンテンツを投入していきます。

まずは、設定画面のメインとなるComponentにタイトルのみを記述し、ルートDOMにレンダリングしてみましょう。

コードを記述する前に、 npm run start スクリプトを実行し、コードの変更を監視して自動でビルドされるようにしておいて下さい。

// 設定画面用スタイル
import './admin.scss';

// renderメソッドのインポート
import { render } from '@wordpress/element';

// Adminコンポーネント
const Admin = () => {
    return (
        <div className="wrap">
            <h1>オプション設定</h1>
        </div>
    );
};

// AdminコンポーネントをルートDOMにレンダリング
render(
    <Admin />,
    document.getElementById( 'my-gutenberg-admin-plugin' )
);

正しく記述されていれば、以下のようにタイトルがレンダリング・表示されている事が確認出来ます。

タイトルがレンダリング・表示されている

STEP5:Componentを使った設定項目のレンダリング

本格的な開発に入る前に、改めて今回のプラグインの概要を再確認してみましょう。

  • 全ページの右下に、固定レイアウトで広告を表示する
  • 広告表示の有無、テキスト、文字サイズを設定画面で変更出来る

以上を踏まえて、各設定項目で使うComponentと仕様を以下のように決定しました。

設定項目名初期値入力形式Component
広告を表示するbooleantrueチェックボックス(トグル)ToggleControl
テキストstringここにテキストが入りますテキストボックスTextControl
文字サイズnumber16レンジRangeControl

attributesを持ったカスタムブロックを開発された方にとってはおなじみのComponentばかりですが、これをメインとなるAdminコンポーネント内に追加していきます。
ここでは、各Componentのpropsは最低限のものだけを指定しておきます。

// 設定画面用スタイル
import './admin.scss';

// renderメソッドのインポート
import { render } from '@wordpress/element';

// (追加)Componentのインポート
import {
    ToggleControl,
    TextControl,
    RangeControl
} from '@wordpress/components';

// Adminコンポーネント
const Admin = () => {
    return (
        <div className="wrap">
            <h1>オプション設定</h1>

            {/* (追加)各設定項目のコンポーネント */}
            <ToggleControl
                label="広告を表示する"
            />
            <TextControl
                label="テキスト"
            />
            <RangeControl
                label="文字サイズ"
                min="10"
                max="30"
            />
        </div>
    );
};

// AdminコンポーネントをルートDOMにレンダリング
render(
    <Admin />,
    document.getElementById( 'my-gutenberg-admin-plugin' )
);

ここで、追加したコンポーネントの表示を確認してみます。

追加したコンポーネントの表示を確認

h1見出しの下に余白が無かったり、コンポーネントのバランスが悪かったりするので、CSSで調整しておきます。

#my-gutenberg-admin-plugin {
    h1{
        margin-bottom: 2em;
    }
    .components-base-control {
        max-width: 400px;
        margin-bottom: 20px;
    }
}
追加したコンポーネントの表示を確認

STEP6:設定値のstate管理

各設定項目で入力・変更された値を保持するために、stateを導入します。
stateとは、ざっくりと言うと「そのコンポーネントが持っている状態」の事で、stateで保持された値は、後々のSTEPでの保存処理にも使用します。

まずはstateを利用するために useState フックのインポートと、stateの宣言を行います。
形式は以下の通りです。

import { useState } from '@wordpress/element';
const [ stateの変数名, stateを更新する関数名 ] = useState( stateの初期値 );

「stateを更新する関数名」は、慣例的に「set + stateの変数名」となっています。

そして、stateを更新する関数名が例えば setTextであった場合、stateの更新は以下のように行います。

setText ( '新しいテキスト' );

以上を踏まえて、各設定値のstateの宣言と、各設定項目の初期値、設定項目が入力・変更された時にstateを更新する処理を記述します。
attributesを持ったカスタムブロックを開発された事のある方であれば、「いつもsetAttributesしている箇所をsetXXXに置き換える」と言うとイメージしやすいかもしれません。

// 設定画面用スタイル
import './admin.scss';

import {
    render,

    // (追加)useStateフックのインポート
    useState
} from '@wordpress/element';

// Componentのインポート
import {
    ToggleControl,
    TextControl,
    RangeControl
} from '@wordpress/components';

// Adminコンポーネント
const Admin = () => {

    // (追加)stateと初期値の宣言
    const [ showFlg, setShowFlg ] = useState( true );               // 広告を表示する
    const [ text, setText ] = useState( 'ここにテキストが入ります' ); // テキスト
    const [ fontSize, setFontSize ] = useState( 16 );               // 文字サイズ

    return (
        <div className="wrap">
            <h1>オプション設定</h1>
            <ToggleControl
                label="広告を表示する"

                // (追加)初期値の設定と入力・変更された時のstate更新処理
                checked={ showFlg }
                onChange={ () => setShowFlg( ! showFlg ) }
            />
            <TextControl
                label="テキスト"

                // (追加)初期値の設定と入力・変更された時のstate更新処理
                value={ text }
                onChange={ ( value ) => setText( value ) }
            />
            <RangeControl
                label="文字サイズ"
                min="10"
                max="30"

                // (追加)初期値の設定と入力・変更された時のstate更新処理
                value={ fontSize }
                onChange={ ( value ) => setFontSize( value ) }
            />
        </div>
    );
};

// AdminコンポーネントをルートDOMにレンダリング
render(
    <Admin />,
    document.getElementById( 'my-gutenberg-admin-plugin' )
);

ここで一度ブラウザをリロードし、 useState フックで宣言した初期値が、各設定項目の初期値として反映されている事を確認してください。

各設定項目の初期値として反映されている事を確認

STEP7:設定項目の登録

ここで一旦PHPに戻って、register_setting 関数でオプションページの設定項目を登録・定義します。

~~~~~

add_action( 'admin_enqueue_scripts', 'my_admin_scripts' );

// 設定項目の登録
function my_register_settings() {
    // 広告を表示する
    register_setting(
        'my_gutenberg_admin_plugin_settings',
        'my_gutenberg_admin_plugin_show_flg',
        array(
            'type'         => 'boolean',
            'show_in_rest' => true,
            'default'      => true,
        )
    );
    // テキスト
    register_setting(
        'my_gutenberg_admin_plugin_settings',
        'my_gutenberg_admin_plugin_text',
        array(
            'type'         => 'string',
            'show_in_rest' => true,
            'default'      => 'ここにテキストが入ります',
        )
    );
    // 文字サイズ
    register_setting(
        'my_gutenberg_admin_plugin_settings',
        'my_gutenberg_admin_plugin_font_size',
        array(
            'type'         => 'number',
            'show_in_rest' => true,
            'default'      => 16,
        )
    );
}
add_action( 'init', 'my_register_settings' );

ここでのポイントは、register_settingの第3引数の配列にshow_in_rest => trueを指定するという点です。

今回登録した設定項目は、 get_option などのPHP関数で直接取得するわけではなく、後述のREST APIにて取得するためです。

STEP8:APIを使用した設定値の取得・反映

再度JavaScriptに戻って、「STEP7:設定項目の登録」で設定した設定項目をAPIで取得してみます。

ここはあまり詳しくないのですが、 @wordpress/api パッケージにBackbone JavaScript Clientというライブラリが含まれており、wp_options テーブルに格納されたプラグインの設定値を簡単に取得する事が出来ます。


@wordpress/api の代わりに、@wordpress/api-fetchを使う方法もあります。
その場合、 register_rest_route でカスタムエンドポイントを定義し、コールバック関数で get_option 値を返却する処理を記述する必要があります。
より柔軟に設定値を扱う事が出来るので、興味がある方は調べてみて下さい。


ひとまず、どのようなレスポンスが返ってくるかを確認してみましょう。

// 設定画面用スタイル
import './admin.scss';

import {
    render,
    useState
} from '@wordpress/element';

// Componentのインポート
import {
    ToggleControl,
    TextControl,
    RangeControl
} from '@wordpress/components';

// (追加)APIのインポート
import api from '@wordpress/api';

// Adminコンポーネント
const Admin = () => {

    // stateと初期値の宣言
    const [ showFlg, setShowFlg ] = useState( true );               // 広告を表示する
    const [ text, setText ] = useState( 'ここにテキストが入ります' ); // テキスト
    const [ fontSize, setFontSize ] = useState( 16 );               // 文字サイズ

    // (追加)クライアントの準備が出来てから実行
    api.loadPromise.then( () => {

        // Modelの生成
        const model = new api.models.Settings();

        // 設定値の取得
        model.fetch().then( response => {
            console.log( response );
        });
    });

    return (
        <div className="wrap">
            <h1>オプション設定</h1>
            <ToggleControl
                label="広告を表示する"
                checked={ showFlg }
                onChange={ () => setShowFlg( ! showFlg ) }
            />
            <TextControl
                label="テキスト"
                value={ text }
                onChange={ ( value ) => setText( value ) }
            />
            <RangeControl
                label="文字サイズ"
                min="10"
                max="30"
                value={ fontSize }
                onChange={ ( value ) => setFontSize( value ) }
            />
        </div>
    );
};

// AdminコンポーネントをルートDOMにレンダリング
render(
    <Admin />,
    document.getElementById( 'my-gutenberg-admin-plugin' )
);
APIを使用した設定値の取得

register_setting 関数で定義した設定項目とデフォルト値を含め、 wp_options テーブルのデータが取得された事を確認出来ます。

続いて、取得した設定値を各設定項目に反映させます。

STEP6:設定値のstate管理」で記載した通り、各設定項目の値はstateで管理していますので、setXXXX の引数にレスポンスデータの値を指定すれば、各設定項目の値として反映されるはずです。

// 設定画面用スタイル
import './admin.scss';

import {
    render,
    useState,

    // (追加)useEffectフックのインポート
    useEffect
} from '@wordpress/element';

// Componentのインポート
import {
    ToggleControl,
    TextControl,
    RangeControl
} from '@wordpress/components';

// (追加)APIのインポート
import api from '@wordpress/api';

// Adminコンポーネント
const Admin = () => {

    // stateと初期値の宣言
    const [ showFlg, setShowFlg ] = useState( true );               // 広告を表示する
    const [ text, setText ] = useState( 'ここにテキストが入ります' ); // テキスト
    const [ fontSize, setFontSize ] = useState( 16 );               // 文字サイズ

    // (追加)取得した設定値をstateに反映
    useEffect( () => {
        api.loadPromise.then( () => {
            const model = new api.models.Settings();
            model.fetch().then( response => {
                setShowFlg( Boolean( response.my_gutenberg_admin_plugin_show_flg ) );
                setText( response.my_gutenberg_admin_plugin_text );
                setFontSize( response.my_gutenberg_admin_plugin_font_size );
            });
        });
    }, []);

    return (
        <div className="wrap">
            <h1>オプション設定</h1>
            <ToggleControl
                label="広告を表示する"
                checked={ showFlg }
                onChange={ () => setShowFlg( ! showFlg ) }
            />
            <TextControl
                label="テキスト"
                value={ text }
                onChange={ ( value ) => setText( value ) }
            />
            <RangeControl
                label="文字サイズ"
                min="10"
                max="30"
                value={ fontSize }
                onChange={ ( value ) => setFontSize( value ) }
            />
        </div>
    );
};

// AdminコンポーネントをルートDOMにレンダリング
render(
    <Admin />,
    document.getElementById( 'my-gutenberg-admin-plugin' )
);

register_setting 関数で各設定値のdefault値を設定したかと思いますが、それらを他の値に変更してブラウザをリロードした時に、各設定項目に変更後のdefault値が反映される事を確認してください。

またここで、今までになかった useEffect というフックがインポートされ、設定値を取得する処理を囲んでいる事が分かります。

ざっくり言うと、React Hooks の一つで、「関数を特定のタイミングで実行させる」事が出来ます。

useEffect の第二引数には、依存配列を指定する事ができ、特定のpropsやstateが変わった時に実行、といった事を行う事が出来ます。

今回のコードでは空配列([])を指定していますが、この場合はComponentがレンダリングされた直後に一度だけ実行されます。
React のライフサイクルを知っている方は、クラスコンポーネントの componentDidMountcomponentDidUpdatecomponentWillUnmount がまとまったもの、と言えば分かりやすいかもしれません。

useEffectを使わないと、ComponentのレンダリングとAPIレスポンスのどちらが先に完了するかが保証されないため、想定外のエラーが発生する事があります。

useEffectを使う事で、「Componentのレンダリングが完了してからAPIを実行する」と同期をとった処理を行う事が出来ます。

今回のサンプルでは useEffect が無くても動作すると思いますが、細かな非同期処理やstate管理などを行う場合に有用なフックです。

Gutenbergの標準ブロックでも多数の利用例があるので、興味がある方は研究してみて下さい。

gutenberg/packages/block-library/src at trunk · WordPress/gutenberg

STEP9:APIを使用した設定値の更新

今度は、手動で適当な値をベタ書きし、API経由で設定値の登録を行ってみます。

// 設定画面用スタイル
import './admin.scss';

import {
    render,
    useState,
    useEffect
} from '@wordpress/element';

// Componentのインポート
import {
    ToggleControl,
    TextControl,
    RangeControl
} from '@wordpress/components';

// APIのインポート
import api from '@wordpress/api';

// Adminコンポーネント
const Admin = () => {

    // stateと初期値の宣言
    const [ showFlg, setShowFlg ] = useState( true );               // 広告を表示する
    const [ text, setText ] = useState( 'ここにテキストが入ります' ); // テキスト
    const [ fontSize, setFontSize ] = useState( 16 );               // 文字サイズ

    // 取得した設定値をstateに反映
    useEffect( () => {
        api.loadPromise.then( () => {
            const model = new api.models.Settings();
            model.fetch().then( response => {
                setShowFlg( Boolean( response.my_gutenberg_admin_plugin_show_flg ) );
                setText( response.my_gutenberg_admin_plugin_text );
                setFontSize( response.my_gutenberg_admin_plugin_font_size );
            });
        });
    }, []);

    // (追加)設定項目の登録
    api.loadPromise.then( () => {
        const model = new api.models.Settings({
            'my_gutenberg_admin_plugin_show_flg': false,
            'my_gutenberg_admin_plugin_text': 'API経由で登録されたテキストです',
            'my_gutenberg_admin_plugin_font_size': 20
        });

        const save = model.save();

        save.success( ( response, status ) => {
            console.log( response );
            console.log( status );
        });
        save.error( ( response, status ) => {
            console.log( response );
            console.log( status );
        });
    });

    return (
        <div className="wrap">
            <h1>オプション設定</h1>
            <ToggleControl
                label="広告を表示する"
                checked={ showFlg }
                onChange={ () => setShowFlg( ! showFlg ) }
            />
            <TextControl
                label="テキスト"
                value={ text }
                onChange={ ( value ) => setText( value ) }
            />
            <RangeControl
                label="文字サイズ"
                min="10"
                max="30"
                value={ fontSize }
                onChange={ ( value ) => setFontSize( value ) }
            />
        </div>
    );
};

// AdminコンポーネントをルートDOMにレンダリング
render(
    <Admin />,
    document.getElementById( 'my-gutenberg-admin-plugin' )
);
APIを使用した設定値の更新

ブラウザをリロードすると、API経由で設定値の更新処理が行われた後に save.success() が呼ばれ、レスポンスに手動で設定した新しい値が反映されている事が確認出来ます。

※ちなみに、 register_setting で定義したものと違う型のデータ( 'my_gutenberg_admin_plugin_show_flg': 'hoge' など)を登録しようとすると、500エラーとなり save.error() の方が呼ばれます。

データベースの wp_options テーブル

データベースの wp_options テーブルにも、ちゃんとレコードが登録されている事が確認出来ます。

STEP10:state管理した設定値の登録

STEP8:APIを使用した設定値の取得」と「STEP9:APIを使用した設定値の更新」を組み合わせて、実際に設定画面で入力された値(state)を元に、API経由で設定値を更新する処理を行います。

実装方針として、保存ボタンを追加し、押下された時にComponentが持つstateをAPIのmodelに格納して保存処理を行います。

保存ボタンを追加し、onClickイベント内に先ほど追加した設定項目の登録処理を入れ込みます。
そして、ベタ書きしていた各プロパティの値をstate値に置き換えます。
@wordpress/components から Button コンポ―ネントを追加インポートしておく事も忘れないでください。

// 設定画面用スタイル
import './admin.scss';

import {
    render,
    useState,
    useEffect
} from '@wordpress/element';

// Componentのインポート
import {
    ToggleControl,
    TextControl,
    RangeControl,

    // (追加)Buttonコンポーネント
    Button
} from '@wordpress/components';

// APIのインポート
import api from '@wordpress/api';

// Adminコンポーネント
const Admin = () => {

    // stateと初期値の宣言
    const [ showFlg, setShowFlg ] = useState( true );               // 広告を表示する
    const [ text, setText ] = useState( 'ここにテキストが入ります' ); // テキスト
    const [ fontSize, setFontSize ] = useState( 16 );               // 文字サイズ

    // 取得した設定値をstateに反映
    useEffect( () => {
        api.loadPromise.then( () => {
            const model = new api.models.Settings();
            model.fetch().then( response => {
                setShowFlg( Boolean( response.my_gutenberg_admin_plugin_show_flg ) );
                setText( response.my_gutenberg_admin_plugin_text );
                setFontSize( response.my_gutenberg_admin_plugin_font_size );
            });
        });
    }, []);

    // (変更) 設定項目の登録
    const onClick = () => {
        api.loadPromise.then( () => {
            const model = new api.models.Settings({
                'my_gutenberg_admin_plugin_show_flg': showFlg,  // stateの値
                'my_gutenberg_admin_plugin_text': text,         // stateの値
                'my_gutenberg_admin_plugin_font_size': fontSize // stateの値
            });
            const save = model.save();

            save.success( ( response, status ) => {
                console.log( response );
                console.log( status );
            });
            save.error( ( response, status ) => {
                console.log( response );
                console.log( status );
            });
        });
    };

    return (
        <div className="wrap">
            <h1>オプション設定</h1>
            <ToggleControl
                label="広告を表示する"
                checked={ showFlg }
                onChange={ () => setShowFlg( ! showFlg ) }
            />
            <TextControl
                label="テキスト"
                value={ text }
                onChange={ ( value ) => setText( value ) }
            />
            <RangeControl
                label="文字サイズ"
                min="10"
                max="30"
                value={ fontSize }
                onChange={ ( value ) => setFontSize( value ) }
            />
            {/* (追加)保存ボタン */}
            <Button
                isPrimary
                onClick={ onClick }
            >
                保存
            </Button>
        </div>
    );
};

// AdminコンポーネントをルートDOMにレンダリング
render(
    <Admin />,
    document.getElementById( 'my-gutenberg-admin-plugin' )
);

設定項目の値を何度か変えて、保存ボタンを押下する度に最新の値が save.success() のレスポンスに反映され、また wp_options テーブルの各レコードの値も更新される事を確認してください。

以上が、React(Gutenberg Components)を使ってプラグインの設定画面を作る基本的な方法となります。

応用編

ここまでで開発したプラグインはあくまでサンプルコードのため、実運用に耐えうるクオリティではありません。

改善点は多々考えられますが、「この記事の内容では物足りない!」という方は、以下の追加開発にも挑戦してみて下さい。
※Gutenberg Componentsに直接関係ない内容も一部含まれています

設定値の無害化

STEP7:設定項目の登録」で、 register_setting 関数を使ってオプションページの設定項目を登録しましたが、サニタイズ処理がありません。

'my_gutenberg_admin_plugin_show_flg': 'hoge' 」のように、型が違う値は弾くことが出来ますが、例えばテキスト( my_gutenberg_admin_plugin_text )の文字長や、文字サイズ( my_gutenberg_admin_plugin_font_size )の下限値/上限値のチェックを行っていません。

register_setting 関数の第3引数の配列に、 sanitize_callback でサニタイズ用コールバック関数を指定出来ますので、各設定値に異常値が入力された場合の処理を追加してみて下さい。

通知メッセージの表示

保存ボタンを押すと設定値が更新されますが、画面に何の反応もないため、ユーザービリティ的によくありません。

設定が正しく保存された時( save.success() )、および設定の保存に失敗した時( save.error() )に、画面に何らかのメッセージを表示する事を検討します。

一例として、 react-notifications-component というnpmパッケージを使って、保存時にメッセージを表示するサンプルを紹介します。

otter-blocks/Main.js at master · Codeinwp/otter-blocks

ローディング状態の表示

API経由で設定値を取得・更新する時に、何らかの理由でレスポンスを得るまでに時間がかかる可能性もあります。

そのような場合、ユーザーに処理を行っている最中である事を伝えるために、ローディング状態であるかどうかの管理、および画面に表示する機能が必要です。

一つのやり方として、 isAPILoading のようなstateを用意し、API通信開始時に true 、API通信終了時に false に更新し、 isAPILoading == true の時にローディング状態を表示する、という方法があります。

ローディング状態を知らせるのに最適なComponentの一つに、 @wordpress/componentsSpinner があります。

このComponentを使ったコード例を紹介します。

otter-blocks/Main.js at master · Codeinwp/otter-blocks

初期化機能の追加

設定値を色々と変更している間に、よく分からなくなってしまったので初期値に戻したい、というケースがあると思います。

それに対応するために、リセットボタンの追加を検討します。

色々調べてみましたが、保存ボタンで行ったように @wordpress/apinew api.models.Settings を使って、 delete_option 的な処理を行う方法は見つかりませんでした。
(ご存じの方がいましたらコメントください。。!)

自分の場合は、@wordpress/api-fetch を使い、 register_rest_route でカスタムエンドポイントを定義し、コールバック関数内で delete_option を実行する事で対応しました。

手前味噌になりますが、以下を参考にしてみて下さい。

また、ボタンを押下した時にいきなり初期化処理を行うのではなく、一旦確認のためにポップアップウィンドウを挟む方が親切です。

自前で作らなくても、 Modal という便利なComponentが用意されています。

Modal | Block Editor Handbook | WordPress Developer Resources

設定値をAPI経由ではなくJavaScript変数から取得する

各設定値を初期値から変更・保存している場合、ページ読み込み時の設定値(state)が以下のように変更されます。

  1. useState で指定した初期値
  2. API経由で取得した設定値をsetStateで設定する

1と2の間にはタイムラグがあるため、ページ読み込み直後の挙動を確認してみると、設定項目の入力欄の値が「1の値→2の値」と変わる事が分かります。

今回は、APIでの設定値取得のためにあえてこのような仕様にしましたが、wp_localize_script関数を使用してページ読み込み時の設定値を取得する方法もあります。

wp_localize_script は、PHPで設定した値をJavaScriptの変数として出力出来る関数で、以下の手順で実装すればタイムラグを解消でき、また設定値取得のためのAPI通信処理も無くす事が出来ます。

  1. admin_enqueue_scripts でフックされた関数内で、get_optionを使用して設定値を取得する
  2. wp_localize_script 関数の第2引数に変数名、第3引数の配列に設定値を指定する
  3. useState の初期値に、JavaScriptで出力された変数の値を使う

参考文献

  1. Making a “Plugin Options Page” With Gutenberg Components

    この記事のベースとなった、チュートリアル形式の記事です。
  2. Gutenberg Blocks and Template Library by Otter

    1.の記事のサンプルコードのベースにもなっているプラグインです。
    非常にシンプルなコードなので、実際のプラグイン開発ではどのように実装されているか大変参考になります。
  3. https://github.com/Automattic/jetpack/tree/master/projects/plugins/jetpack

    WordPress公式(Automatic)が提供している高機能なプラグインです。
    その分コードも複雑で自分も理解しきれていませんが、カスタムブロックも含めた複合的なプラグイン開発の参考になると思います。