1. ホーム
  2. WordPress

【WordPress】InspectorControls と PanelBody でブロックの設定サイドバーを作る方法

Share

カスタムブロックを作っていると、本文の入力欄だけでは足りなくなってきます。「アイコンを出すか消すか」「配色のパターンを切り替えたい」「余白を数値で調整したい」といった設定は、本文の中に置くよりも、コアのブロックと同じように画面右側の設定サイドバーに並べたほうが自然です。このサイドバーへ項目を差し込む仕組みが InspectorControls で、その中を見出し付きの開閉パネルに整理するのが PanelBody です。この記事では block.json の属性定義から edit.js でのサイドバー実装、save.js への反映までを、動く一連のコードとして解説します。WordPress 6 系、apiVersion 3 を前提としています。

InspectorControls は右サイドバーへの差し込み口

InspectorControls@wordpress/block-editor が提供するコンポーネントで、その中に書いた UI が編集画面右側の「ブロック」タブに表示されます。特徴的なのは、書いた場所と表示される場所が違うことです。edit の返り値の中に書くのに、その位置に描画されるわけではなく、サイドバーへ運ばれて表示されます。エディター側があらかじめ用意している差し込み口に、ブロック側から中身を送り込むような形になっています。

そのため InspectorControlsedit が返す要素の中に含まれてさえいれば、ブロック本体の前でも後ろでも構いません。表示されるのはそのブロックが選択されているあいだだけで、選択が外れると自動的に消えます。表示・非表示の制御をこちらで書く必要はありません。

サイドバーに差し込んだ項目は、最終的に attributessetAttributes で書き換えるためのものです。流れとしては次の 3 ステップになります。

  1. block.json に設定値を入れる属性を宣言する
  2. edit.jsInspectorControls の中にコントロールを置き、onChange から setAttributes を呼ぶ
  3. save.js でその属性を読み、クラス名やスタイルとして出力する

block.json に設定用の属性を用意する

ここでは例として、お知らせを目立たせる webool/feature-box というブロックを作ります。本文のほかに「アイコンの表示・非表示」「配色パターン」「内側の余白」の 3 つを設定できるようにします。それぞれの値を入れる属性を block.json に宣言しておきます。

src/block.json
{
    "$schema": "https://schemas.wp.org/trunk/block.json",
    "apiVersion": 3,
    "name": "webool/feature-box",
    "title": "注目ボックス",
    "category": "design",
    "textdomain": "webool",
    "attributes": {
        "content": {
            "type": "string",
            "source": "html",
            "selector": "p.feature-box__text",
            "default": ""
        },
        "showIcon": {
            "type": "boolean",
            "default": true
        },
        "boxStyle": {
            "type": "string",
            "default": "info"
        },
        "padding": {
            "type": "number",
            "default": 24
        }
    },
    "editorScript": "file:./index.js",
    "style": "file:./style-index.css"
}

設定用の属性には source を付けていません。source なしの属性は、ブロックコメントの中に JSON として保存されます。boxStylepadding はクラス名やスタイルの形で HTML に現れますが、そこから読み戻す必要はないので、素直に JSON として持たせるのがシンプルです。default を書いておくと、ブロックを挿入した直後からサイドバーの各コントロールに初期値が入った状態になります。

edit.js で InspectorControls と PanelBody を書く

実際にサイドバーを作ります。InspectorControls@wordpress/block-editor から、パネルと各コントロールは @wordpress/components から読み込みます。

src/edit.js
import {
    useBlockProps,
    InspectorControls,
    RichText,
} from '@wordpress/block-editor';
import {
    PanelBody,
    ToggleControl,
    SelectControl,
    RangeControl,
} from '@wordpress/components';

export default function Edit( { attributes, setAttributes } ) {
    const { content, showIcon, boxStyle, padding } = attributes;

    const blockProps = useBlockProps( {
        className: `feature-box feature-box--${ boxStyle }`,
        style: { padding: `${ padding }px` },
    } );

    return (
        <>
            {/* この中身はサイドバーに表示される */}
            <InspectorControls>
                <PanelBody title="表示設定" initialOpen={ true }>
                    <ToggleControl
                        __nextHasNoMarginBottom
                        label="アイコンを表示する"
                        help={ showIcon ? '左側にアイコンが付きます' : 'テキストのみ表示します' }
                        checked={ showIcon }
                        onChange={ ( value ) =>
                            setAttributes( { showIcon: value } )
                        }
                    />

                    <SelectControl
                        __nextHasNoMarginBottom
                        label="配色"
                        value={ boxStyle }
                        options={ [
                            { label: '情報(青)', value: 'info' },
                            { label: '注意(黄)', value: 'warning' },
                            { label: '成功(緑)', value: 'success' },
                        ] }
                        onChange={ ( value ) =>
                            setAttributes( { boxStyle: value } )
                        }
                    />

                    <RangeControl
                        __nextHasNoMarginBottom
                        label="内側の余白(px)"
                        value={ padding }
                        min={ 0 }
                        max={ 64 }
                        step={ 4 }
                        allowReset
                        resetFallbackValue={ 24 }
                        onChange={ ( value ) =>
                            // リセット時に undefined が渡ることがある
                            setAttributes( { padding: value ?? 24 } )
                        }
                    />
                </PanelBody>
            </InspectorControls>

            {/* こちらは通常どおり本文の位置に描画される */}
            <div { ...blockProps }>
                { showIcon && (
                    <span className="feature-box__icon" aria-hidden="true">
                        !
                    </span>
                ) }
                <RichText
                    tagName="p"
                    className="feature-box__text"
                    value={ content }
                    onChange={ ( value ) =>
                        setAttributes( { content: value } )
                    }
                    placeholder="お知らせの本文を入力"
                />
            </div>
        </>
    );
}

コントロール 3 つの書き方はどれも同じ形です。現在の属性値を checked または value に渡して表示し、変更されたときに呼ばれる onChange の中で setAttributes を呼んで属性を更新する。コントロール自身は値を持たず、表示も更新もこちらが用意した属性に委ねられています。RichText と同じ考え方です。

<>...</> というフラグメントで囲んでいるのは、InspectorControls とブロック本体という 2 つの要素を返す必要があるためです。InspectorControls をブロック本体の div の内側に書いても動作しますが、外に出しておいたほうが「これは本文ではない」ことが読み取りやすくなります。

PanelBody で開閉できるグループを作る

PanelBody は、見出しをクリックすると中身が開閉するパネルです。コアのブロックでサイドバーに「設定」「色」「タイポグラフィ」といった見出しが並んでいますが、あれと同じ見た目になります。設定項目が増えてきたら、性質ごとに PanelBody を分けて並べると探しやすくなります。

prop説明
titleパネルの見出しに表示する文字列。省略すると開閉ボタン自体が出ず、常に開いた状態になる
initialOpen最初に開いた状態にするかどうか。既定は true。開閉の状態はパネル自身が保持する
opened開閉状態を外部から制御したいときに渡す。渡すと initialOpen より優先され、状態は渡した値に従う
onToggle見出しがクリックされたときに呼ばれる関数。opened と組み合わせて使う
icon見出しの横に表示するアイコン
classNameパネルに追加するクラス名

ふだんは titleinitialOpen だけで足ります。よく使う設定のパネルは initialOpen={ true }、めったに触らない詳細設定は initialOpen={ false } にしておく、といった使い分けです。opened を渡すと開閉の管理がこちら側の責任になるので、onToggle で状態を更新する処理まで書かないと開かなくなります。特別な理由がなければ initialOpen だけを使うほうが安全です。

なお、1 つのパネルの中に横並びで項目を置きたいときは PanelRow で囲みます。ラベルと入力欄を 1 行にまとめたい場合に使います。

__nextHasNoMarginBottom は何のためのものか

上のコードで各コントロールに付けている __nextHasNoMarginBottom は、将来のスタイルを先に適用するためのオプトイン用の prop です。@wordpress/components のコントロールは長らく下方向のマージンを自前で持っていましたが、余白はレイアウト側で管理するほうが扱いやすいため、それを取り除く方向へ移行しています。

WordPress 6.7 以降のブロックエディターでは、この prop を付けずにコントロールを使うと、ブラウザーのコンソールに「__nextHasNoMarginBottomtrue にして新しいスタイルへ移行してほしい」という趣旨の非推奨(deprecated)の警告が出ます。動かなくなるわけではありませんが、新しく書くコードでは付けておくと、将来デフォルトが切り替わったときに見た目が変わらずに済みます。true を明示せず prop 名だけを書けば true を渡したことになります。

同じ系統のものに __next40pxDefaultSize があります。こちらはコントロールの高さを新しい既定サイズ(40px)に合わせるためのもので、SelectControlTextControl など入力系のコントロールで使います。二重アンダースコアで始まる prop は、いずれも「次のメジャーな挙動を先に有効にする」という意味の一時的なフラグだと考えてください。

用途に合わせてコントロールを選ぶ

@wordpress/components には設定サイドバー向けのコントロールが一通りそろっています。属性の type に合わせて選ぶと迷いません。

コンポーネント用途と主な props
ToggleControlオン・オフの切り替え。boolean の属性に使う。値は value ではなく checked で渡す
CheckboxControl同じく boolean 向け。チェックボックスの見た目。checkedonChange
SelectControl選択肢から 1 つ選ぶ。valueoptions{ label, value } の配列)。onChange には文字列が渡る
RangeControl数値をスライダーで指定。value / min / max / step、リセットボタンを出す allowReset
TextControl1 行のテキスト入力。URL やクラス名など短い文字列に
TextareaControl複数行のテキスト入力
ToggleGroupControlボタンが横に並ぶ形の選択。選択肢が 2〜3 個で短いラベルのときに向く

色を選ばせたいときは @wordpress/block-editor 側の PanelColorSettings を使うと、テーマのカラーパレットが載ったパネルをそのまま出せます。ただし、文字色・背景色・余白・枠線といった汎用的な装飾は、自前のコントロールを作るより block.jsonsupportscolorspacing を書いてエディター標準の UI に任せたほうが、テーマとの相性もよく手間もかかりません。自作のコントロールは、そのブロック固有の設定に絞るのがおすすめです。

タブを指定して差し込む

サイドバーには「設定」と「スタイル」のタブがあります。InspectorControls は既定で「設定」タブに入りますが、group を指定すると差し込み先を変えられます。見た目に関する設定はスタイル側にまとめると、コアのブロックと同じ感覚で使えます。

src/edit.js(差し込み先を分ける)
<InspectorControls>
    <PanelBody title="表示設定">
        {/* 「設定」タブに入る */}
    </PanelBody>
</InspectorControls>

<InspectorControls group="styles">
    <PanelBody title="見た目の調整">
        {/* 「スタイル」タブに入る */}
    </PanelBody>
</InspectorControls>

<InspectorControls group="advanced">
    {/* 「高度な設定」パネルの中に入る。PanelBody で囲まない */}
</InspectorControls>

group="advanced" は「高度な設定」パネルの内側に直接差し込まれるため、PanelBody で囲む必要はありません。追加 CSS クラスの入力欄が並んでいる、あのエリアです。

save.js に設定を反映する

サイドバーで変更した値は属性に入っただけなので、そのままではフロント側の見た目は変わりません。save でも同じ属性を読み、クラス名やスタイルとして書き出します。

src/save.js
import { useBlockProps, RichText } from '@wordpress/block-editor';

export default function save( { attributes } ) {
    const { content, showIcon, boxStyle, padding } = attributes;

    // edit 側と同じ className・style を出力する
    const blockProps = useBlockProps.save( {
        className: `feature-box feature-box--${ boxStyle }`,
        style: { padding: `${ padding }px` },
    } );

    return (
        <div { ...blockProps }>
            { showIcon && (
                <span className="feature-box__icon" aria-hidden="true">
                    !
                </span>
            ) }
            <RichText.Content
                tagName="p"
                className="feature-box__text"
                value={ content }
            />
        </div>
    );
}

ここでのポイントは、editsave で出力する構造をそろえることです。edit ではアイコンを出しているのに save では出していない、クラス名の付け方が違う、といったずれがあると、編集画面とフロントの見た目が食い違います。上のように classNamestyle の組み立て方をそのまま合わせておくと、そこで悩まずに済みます。

フロントの HTML を PHP で組み立てる動的ブロックの場合は、save の代わりに render.php で同じことをします。属性は $attributes 配列に入って渡ってきます。

src/render.php(動的ブロックの場合)
<?php
$wrapper_attributes = get_block_wrapper_attributes(
    array(
        'class' => 'feature-box feature-box--' . esc_attr( $attributes['boxStyle'] ),
        'style' => 'padding:' . intval( $attributes['padding'] ) . 'px',
    )
);
?>
<div <?php echo $wrapper_attributes; ?>>
    <?php if ( ! empty( $attributes['showIcon'] ) ) : ?>
        <span class="feature-box__icon" aria-hidden="true">!</span>
    <?php endif; ?>
    <p class="feature-box__text">
        <?php echo wp_kses_post( $attributes['content'] ); ?>
    </p>
</div>

サイドバーに項目が出ない・設定が反映されないとき

コントロールを追加したのにサイドバーに何も出ない、操作はできるのに保存すると元に戻る、あるいは赤い枠のエラーになる。InspectorControls まわりのつまずきは、原因のパターンがだいたい決まっています。

InspectorControls が edit の返り値に入っていない

サイドバーに何も表示されないときにまず疑うのがこれです。InspectorControls は差し込み口に中身を送る仕組みなので、実際に描画されて初めて働きますedit の中で変数に代入しただけ、あるいは条件分岐で描画されないルートに入ってしまっている場合、サイドバーには何も現れません。エラーも警告も出ないので気づきにくいところです。

フラグメントの書き忘れも同じ症状につながります。ブロック本体と InspectorControls という 2 つの要素を返すには、<>...</> で囲むか、どちらかをもう一方の内側に入れる必要があります。そして当然ですが、そのブロックを選択していないと表示されません。編集画面のどこもクリックしていない状態では投稿全体の設定が出ているので、まずブロックをクリックして「ブロック」タブに切り替わることを確認してください。

block.json に属性がない、またはビルドが古い

コントロールは動くのに、投稿を開き直すと設定が既定値に戻っている。この場合は setAttributes に渡しているキーが block.jsonattributes に宣言されていない可能性が高いです。宣言のない属性に値を渡してもエラーにはならず、単に保存されないまま消えます。コントロールを 1 つ増やすときは、必ず属性の宣言もセットで増やしてください。

宣言してあるのに直らないときは、ビルドの反映漏れを確認します。@wordpress/scripts を使っている場合、エディターが読み込むのは src/block.json ではなく build/block.json です。npm run start を止めたまま src だけ編集していると、古い定義が使われ続けます。build 側のファイルを開いて、追加した属性が入っているかを見るのが確実です。

save を書き換えたあとに「想定されていないエラー」が出る

設定を反映させるために save を書き換えると、それまでに作った既存のブロックが「このブロックには、想定されていないエラーが含まれています」と表示されることがあります。これは投稿に保存されている HTML と、いまの save が生成する HTML が一致しないときに出るブロック検証エラーです。クラス名に feature-box--info を足した、style 属性を追加した、アイコンの span を増やした。どれも保存済みの HTML とのずれを生みます。

まだ公開していない開発中のブロックなら、該当のブロックを削除して入れ直すのがいちばん早い解決です。すでに公開済みの投稿で使っているなら、古い attributessave の組み合わせを deprecated として登録します。エディターは現在の save で検証に失敗したとき、deprecated に登録された定義を順に試し、一致するものが見つかればその属性を読み取って新しい形へ変換してくれます。

src/deprecated.js
import { useBlockProps, RichText } from '@wordpress/block-editor';

// 設定項目を足す前の、古い save の内容をそのまま書く
const v1 = {
    attributes: {
        content: {
            type: 'string',
            source: 'html',
            selector: 'p.feature-box__text',
            default: '',
        },
    },
    save( { attributes } ) {
        const blockProps = useBlockProps.save( {
            className: 'feature-box',
        } );

        return (
            <div { ...blockProps }>
                <RichText.Content
                    tagName="p"
                    className="feature-box__text"
                    value={ attributes.content }
                />
            </div>
        );
    },
};

// 新しいものから順に並べる
export default [ v1 ];
src/index.js
import { registerBlockType } from '@wordpress/blocks';
import metadata from './block.json';
import Edit from './edit';
import save from './save';
import deprecated from './deprecated';

registerBlockType( metadata.name, {
    edit: Edit,
    save,
    deprecated,
} );

登録し忘れたまま save を変えてしまうと、既存の投稿でブロックが壊れた扱いになり、読者に見えている内容まで失われかねません。サイドバーに設定を足す作業は save の出力を変える作業でもある、という点は覚えておいてください。

エディターでは変わるのにフロントで変わらない

編集画面では配色や余白が切り替わるのに、公開ページでは反映されない。この場合、属性は正しく保存できていて、save 側で読んでいないか、CSS が当たっていないかのどちらかです。まず公開ページのソースを見て feature-box--warning のようなクラスが出力されているかを確認します。出力されていれば原因は CSS で、block.jsonstyle で読み込んでいるスタイルシートに、そのクラスの定義があるかを見ます。editorStyle にだけ書いてあると、編集画面でしか効きません。

まとめ

InspectorControls は、edit の中に書いた UI を編集画面右側の設定サイドバーへ差し込むためのコンポーネントです。その内側を PanelBody で囲むと、title の見出しが付いた開閉パネルになり、initialOpen で最初に開くかどうかを決められます。中に置くコントロールは属性の型に合わせて選び、boolean なら ToggleControl、選択肢なら SelectControl、数値なら RangeControl という具合です。どれも現在の値を checkedvalue で渡し、onChangesetAttributes を呼ぶ形は共通しています。新しく書くコードでは __nextHasNoMarginBottom を付けておくと、将来のスタイル変更に備えられます。

作業の順番としては、block.json に属性を宣言する、edit.js でコントロールを置いて更新する、save.js で出力に反映する、の 3 つをセットで進めるのが確実です。とくに save の出力を変えたときは、公開済みの投稿がブロック検証エラーになるので、deprecated の登録を忘れないようにしてください。

参考ページ