Gutenbergブロックエディタから設定値をカスタムストア経由で変更する


  1. Staff Blog
  2. WordPress
  3. Gutenbergブロックエディタから設定値をカスタムストア経由で変更する

アドベントカレンダー参加中!

この記事は「Vektor WordPress Solutions Advent Calendar 2023」の12月22日の記事になります。

VSCodeのウィンドウで常にモニタが埋め尽くされているベクトル鉄道部のまるやまです。こんにちは。

ベクトル開発スタッフのオオシマさんの「VSCodeの拡張機能『Project Manager』で複数のプロジェクトを簡単に切り替える」読みましたか? コンマリもびっくり! VSCodeのウィンドウ地獄にハマっている人はぜひこれで幸せになってください。

ところで、みなさんはベクトル謹製 「交通案内図とバス時刻表」パターンや「鉄道時刻表(名鉄風)」パターンはつかってくれましたか? 近所のバス停の時刻表を打ち込んで、使わなくなったFire タブレットに表示させて壁にかけておくだけで、あら便利! (※個人の感想です)

さて、前置きが長々となりましたが、今回は久しぶりにブロック開発系のネタです。

今回のお題

設定画面にわざわざ行かなくても、ブロックエディター上で設定値を変更したい」という社内からの要望を沸々と感じております。

プラグイン共通の設定値があって、ユーザーがそれを変更する場合、わざわざ設定画面に遷移して値を書き換える必要がありました。編集画面上でブロックに関連する設定はその場で変えたいというわけです。

私は、REST APIのエントリポイントを作成し、読み書きすることをおもいつきました。そうだ、そうしよう。

とはいえ、それだけでは記事にする意味もあまりないので、カスタムストア経由で読み書きさせるというお題で進めたいと思います。ストアを扱っている記事がなかなか見当たらず、自分も苦労しているところなので、恥ずかしながら、勉強がてらアウトプットしてみることにします。ので、誤ったことを書いていたらすみません。と始めに謝っておきます。

もっといい方法があったら @mthaichi までお知らせ頂けたら嬉しいです。(鉄道ネタはありません)

完成イメージ

というお題で試しにつくってみたのが、こんな感じのものです。

ブロックエディタの右側のパネルにある「Options」を押すとモーダルで設定画面が表示され、設定値をその場で変更できるというものです。4つのテキストボックスに表示されているのは設定画面で保存した設定値です。ブロック自体はダミーのようなもので何の機能もありません。

ソースコードはこちら

記事の中でソースコードを紹介していますが、コピーして使う場合はGithubからどうぞ。

register_setting

設定値をREST API経由でやり取りするには、register_setting関数を使って設定値を定義します。
この時、show_in_restをtrueにする必要があるのですが、以下のようにschemaを定義しておくと、後々いろいろと幸せになれるかなと思って、そうしています。

ここでは vk_custom_store_test_settings という名前でオブジェクト型で vk_option_text_1 から vk_option_text_4までの値を保存するという定義になっています。

add_action('admin_init', 'vk_custom_store_test_settings');

function vk_custom_store_test_settings() {
    register_setting('vk-custom-store-test-settings-group', 'vk_custom_store_test_settings',
	array(
		'type'              => 'object',
		'sanitize_callback' => 'vk_sanitaize_options',
		'show_in_rest'      => array(
			'schema' => array(
				'type'       => 'object',
				'properties' => array(
					'vk_option_text_1'        => array(
						'type' => 'string',
					),
					'vk_option_text_2'        => array(
						'type' => 'string',
					),		
					'vk_option_text_3'        => array(
						'type' => 'string',
					),	
					'vk_option_text_4'        => array(
						'type' => 'string',
					)												
			)
		)
	)));
}

REST APIエントリポイントを定義する

次にREST APIのエントリポイントを作成します。

add_action( 'rest_api_init', 'vk_custom_store_test_rest_api_init' );

function vk_custom_store_test_rest_api_init() {
	register_rest_route(
		'vk-custom-store-test/v2',
		'/settings',
		array(
			array(
				'methods'             => 'GET',
				'callback'            => 'get_vk_option_tests',
				'permission_callback' => function () {
					return current_user_can( 'edit_theme_options' );
				}				
			),
			array(
				'methods'             => 'POST',
				'callback'            => 'update_vk_option_tests',
				'permission_callback' => function () {
					return current_user_can( 'edit_theme_options' );
				}
			)
		)
	);
}

設定値のgetとupdateの2つのエントリポイントを用意します。

ここで permission_callback を忘れないように定義します。この設定値を書き換える権限を指定します。たとえば何らかのライセンスキーやAPIキーを保存させるとして、permission_callback を指定しなかったら、キーがダダ漏れになってしまいます。

callback に定義した関数は以下のとおりです。

function get_vk_option_tests() {
	$options = get_option('vk_custom_store_test_settings');
	return rest_ensure_response( $options );
}

function update_vk_option_tests( $request ) {
	$json_params = $request->get_json_params();
	update_option( 'vk_custom_store_test_settings', $json_params );
	return rest_ensure_response(
		array(
			'success' => true,
		)
	);
}

get_option() / update_option()を使い、設定値を取得・更新しています。

これでREST APIで読み書きできるようになりました。

await apiFetch({
    path: 'vk-custom-store-test/v2/settings',
    method: 'POST',
    data: {
       vk_option_text_1: "あああ",
       vk_option_text_2: "あああ",
       vk_option_text_3: "あああ",
       vk_option_text_4: "あああ",
   }
});

こんな感じでapiFetchすると設定値を書き換えることができます。

カスタムストアを作る

小さい規模のプラグインならば、apiFetch を用いたシンプルな読み書きで十分です。

しかし、プロジェクトが大規模になり、多くのコンポーネントが設定値を共有するような状況では、より洗練されたデータ管理方法が求められます。特にチームでの開発では、開発者の技術レベルや経験が異なるため、一貫性のある状態管理を実現し、全員が簡単に設定値にアクセスできる環境を整えることが重要です。そこでカスタムストアの登場です。

WordPrssの「ストア」とは

ストア(store) は、Gutenbergにおいて、WordPressに関するありとあらゆるデータを扱う保管庫のようなものです。「データ」は記事の情報だけではなく、「今、設定パネルは開いているか」「どんなブロックが配置されているか」といったありとあらゆる情報が格納されており、わたしたちはストアを通して、データを読み書きすることができます。

WordPressの編集画面でデバッグコンソールを開いて、次のように打ち込んでみてください。

wp.data.dispatch('core/notices').createSuccessNotice('Success!');

タイトルの上に、緑色の成功を示すnoticeが表示されました。’core/notices’というストアからcreateSuccessNotice という「アクション」をストアにディスパッチ(送信)することで、WordPressの noticeの情報が書き換わり、それに応じて noticeが表示されます。

ブロック開発をしている人はこんなコードをよく見ると思います。

	const taxonomies = useSelect((select) => {
		return select('core').getTaxonomies({ per_page: -1 }) || [];
	}, []);

これはストアからタクソノミーの一覧を取り出しているコードです。
useDispatchもよくみかけますが、大雑把にuseSelect/selectは取得時、useDispatch/dispatchは更新時に使うとでも覚えておくとよいです。

WordPressのストアは「Redux」というライブラリをベースにしています。reduxについて書き始めると長くなってしまうので、端折りつつ、設定値を管理するカスタムストアのコードを紹介します。

利用側からこんな風に設定値の読み書きができることを目指します。

// 設定値の読み込み
const options = select('vk-custom-store-test/options').getOptions();

// 設定値の更新
dispatch('vk-custom-store-test/options').saveOptions(options);

カスタムストアを作成するには、以下のページのコードが参考になります。

これを参考にして書いたコードがこちらです。

import { createReduxStore, register } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';

// デフォルトのステートを定義
const DEFAULT_STATE = {
    options: {}
};

// アクションタイプを定義
const HYDRATE_OPTIONS = 'HYDRATE_OPTIONS';

// Reduxストアを作成
const store = createReduxStore('vk-custom-store-test/options', {
    // リデューサー
    reducer(state = DEFAULT_STATE, action) {
        switch (action.type) {
            // オプションの更新
            case HYDRATE_OPTIONS:
                return {
                    ...state,
                    options: action.value
                }
        }
        return state;
    },

    // アクション
    actions: {
        // オプションを更新するアクション
        hydrateOptions(values) {
            return {
                type: HYDRATE_OPTIONS,
                value: values,
            };            
        },
        // オプションを保存し、その後更新するアクション
        saveOptions(options) {
            return async ({ dispatch }) => {
                await apiFetch({
                    path: 'vk-custom-store-test/v2/settings',
                    method: 'POST',
                    data: options

                });
                dispatch.hydrateOptions(options);
            }
        },        
    },

    // セレクター
    selectors: {
        // ステートからオプションを取得
        getOptions(state) {
            return state.options;
        },
    },
    // リゾルバー
    resolvers: {
        // オプションを非同期で取得し、その後更新する
        getOptions() {
            return async ({ dispatch }) => {
                try {
                    const options = await apiFetch({
                        path: 'vk-custom-store-test/v2/settings',
                        method: 'GET'
                    });
                    dispatch.hydrateOptions(options);
                } catch (error) {
                    // エラー処理が必要な場合はここに記述
                }
            }
        }       
    }

});

// ストアを登録
register(store);

selectorsgetOptionsが取得時の窓口です。ステートからデータを取り出して返しているだけですが、ステートにデータがない場合は、resolversの同じ名前の関数内で非同期処理でAPIから取得を行います。取得後、hydrateOptionsをディスパッチすることで、取得した設定値がステートに保存されます。

actionshydrateOptionsはアクションクリエーターというもので、アクション名と設定値のオブジェクトを返します。それをreducerが受け取ってステートに設定値をセットします。

actionssaveOptionsが更新時の窓口です。apiFetchでAPIに更新依頼を投げて、取得時同様、hydrateOptionsをディスパッチしてステートを更新しています。

公式ドキュメントの例と比較するとアクションのsaveOptionsresolversの書き方が異なりますが、最近のGutenbergででは非同期のアクションを容易に扱える「サンク(Thunks)」が使えます。処理の流れが明瞭になるので、こちらを採用しています。

ストアを通した設定値の読み書きを実装

さらに、Saveボタンを押すまでの状態をローカルのステートで管理するためのフックを用意しました。
use-options-data.js といった名前で作成します。

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

const useOptionsData = () => {
    // ローカルステートの設定
    const [options, setOptions] = useState({}); // 現在の編集状態を保持するステート
    const [isLoading, setIsLoading] = useState(true); // ローディング状態を管理するステート

    // ストアからのデータを取得する
    const storeData = useSelect((select) => {
        const store = select('vk-custom-store-test/options'); // 特定のストアを選択
        return {
            options: store.getOptions(), // オプションデータを取得
            isLoading: store.isResolving('getOptions'), // データ取得の解決状態を取得
        };
    });

    // ストアからのデータをローカルステートにセット(初期読み込み)
    useEffect(() => {
        // ストアデータのローディングが終了し、ローカルのオプションが空の場合に実行
        if (!storeData.isLoading && storeData.options && Object.keys(options).length === 0) {
            setOptions(storeData.options); // オプションデータをローカルステートに設定
            setIsLoading(false); // ローディング状態を終了
        }
    }, [storeData]);

    // フックからオプションデータとローディング状態を返す
    return { options, setOptions, isLoading };
};

export default useOptionsData;

edit.js

最後に、edit.js を実装します。設定値の読み書き部分がすっきりわかりやすくなっていると思います。

import { __ } from '@wordpress/i18n';
import { useState, useCallback } from '@wordpress/element';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { TextControl, PanelBody, Modal, Button } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import useOptionsData from './use-options-data';
import './editor.scss';

export default function Edit() {
    const [ isOpen, setOpen ] = useState( false );
    const openModal = () => setOpen( true );
    const closeModal = () => setOpen( false );

    // 設定値のstateを取得
    const { options, setOptions, isLoading } = useOptionsData();

    // 設定値をAPIをとおして更新するアクションのディスパッチ
    const { saveOptions } = useDispatch('vk-custom-store-test/options');
    const handleSave = useCallback(() => {
        saveOptions(options).then(() => {
            alert('Settings saved');
        });
    }, [options, saveOptions]);

    return (
        <>
			{ isOpen && 
			<Modal onRequestClose={ closeModal } title="Edit options">
				{isLoading ? <p>Loading...</p> : (
                        <>
                            {Object.keys(options).map((key, index) => (
                                <TextControl
                                    key={index}
                                    label={`Option Text ${index + 1}`}
                                    value={options[key]}
                                    onChange={(nextValue) => {
                                        setOptions({
                                            ...options,
                                            [key]: nextValue,
                                        });
                                    }}
                                />
                            ))}
                            <Button
                                variant="primary"
                                onClick={handleSave}
                                isBusy={isLoading}
                            >
                                Save setting
                            </Button>
							<Button onClick={ closeModal } variant="tertiary">
								Cancel
							</Button>
                        </>
                    )}
            </Modal>
			}
            <InspectorControls>
                <PanelBody title={__('Global Settings')}>
				<Button
                onClick={ openModal }
                variant="primary"
            >Options</Button>
                </PanelBody>
            </InspectorControls>
            <p {...useBlockProps()}>
                {__(
                    'Vk Option Text – hello from the editor!',
                    'vk-option-text'
                )}
            </p>
        </>
    );
}

これで完成です。
Githubに今回のすべてのソースコードを公開しております。

Reduxは慣れてない人には、複雑だと思います。そういう私もまだ慣れていません・・・。

従って、このコードはまだ本番投入できていませんが、もうちょっとブラッシュアップして使えるようにしたいなと考えています。

英語で、ちょっと古いですが、以下の記事も勉強になりました。

WordPress Data Series Overview and Introduction – Unfolding Neurons

For the past couple of years, my involvement with the Gutenberg project in WordPress has frequently taken me into the innards of @wordpress/data.  I’ve had to use it for projects integrating w…

おまけ

今回のお題を調べている中で、以下の記事を見つけて試してみたのですが

Managing Site Settings from the WordPress Block Editor | Blog | Lapero

If you’ve created blocks for the WordPress block editor ‘Gutenberg’ you might be familiar with how those blocks store data as attributes and how to pass that data around to descendent blocks as context. But what about site-wide settings like the title and logo?

サイトタイトルなどはこの方法で書き換えられたのですが、こちらが定義した設定値を書き換えることはできませんでした。今回の例では、編集画面において、一部の設定値を管理者権限ではないユーザーが操作する前提ですので、上記ページの例は使えないのかなと思っております。

Reactベースで構築した設定画面においては、設定値の読み書きはuseEntityPropsaveEditedEntityRecordでサクッとかけそうです。これはもうちょっと深掘りしてどこかで紹介できたらと思います。

また、公式ドキュメントの以下のページが楽しそうです。

お正月休みには、これを手を動かしながら、見てみようと思います。

記事の誤りやもっとこうした方がよいというご指摘やご質問があれば、 @mthaichi までどうぞ!

この記事を書いた人

Taichi Maruyama
Taichi Maruyama
2021年3月からベクトルのお手伝いをしているレガシーエンジニア。最近const覚えました。

その裏で、BREADFISHというキリスト教会専門のウェブ制作事業を密かに展開しています。
フルサイト編集に対応したブロックテーマ X-T9

フルサイト編集対応ブロックテーマ

WordPress テーマ X-T9 は、WordPress 5.9 から実装されたフルサイト編集機能に対応した「ブロックテーマ」と呼ばれる新しい形式のテーマです。
ヘッダーやフッターなど、今までのテーマではカスタマイズが難しかったエリアもノーコードで簡単・柔軟にカスタマイズする事ができます。


PAGE TOP

このデモサイトは Vektor,Inc. のテーマとプラグインで構築されています。ご購入や詳細情報は下記のリンクもご参考ください。