1. ホーム
  2. WordPress

【WordPress】ブロックのフィルターで既存ブロックを拡張する方法|blocks.registerBlockType と editor.BlockEdit を解説

Share

「コアの段落ブロックに、自分のテーマ用のスイッチをひとつ足したい」「見出しブロックにだけ独自のクラスを付けたい」——こうしたとき、わざわざ似たブロックを新しく作る必要はありません。ブロックエディターには、すでに登録されているブロックの設定や編集画面を後から書き換えるフィルターが用意されています。この記事では、属性を追加する blocks.registerBlockType、サイドバーに設定 UI を足す editor.BlockEdit、保存される HTML にクラスを付ける blocks.getSaveContent.extraProps の書き方を、ひとつの例を通して解説します。WordPress 6 系のブロックエディターを前提としています。

ブロックを作り直さずに手を入れる仕組み

ブロックエディターの JavaScript には、PHP の add_filter() とよく似たフックの仕組みがあります。@wordpress/hooks パッケージの addFilter() で「このタイミングの値を、この関数で加工する」と登録しておくと、ブロックが登録されるときや編集画面が描画されるときに自分の関数が呼ばれ、返した値が使われます。

この仕組みのよいところは、対象がコアのブロックでも他人のプラグインのブロックでも同じ書き方が通ることです。元のコードを触らないので、ブロック側が更新されても壊れにくく、フィルターを外せば元の挙動に戻ります。逆に言えば「相手の作りに乗っかっている」ので、対象を絞り込む条件を必ず自分で書く必要があります。

よく使うフィルターは次の4つです。名前の頭が blocks. のものはブロックの定義そのもの、editor. のものはエディターの表示に関わるフィルターです。

フィルター名変えられるもの
blocks.registerBlockTypeブロックの設定そのもの(attributessupports など)
editor.BlockEditエディターの編集画面(設定サイドバーへの追加など)
editor.BlockListBlockエディター上でブロックを包む要素(クラスの追加など)
blocks.getSaveContent.extraProps保存される HTML のルート要素に付く属性

addFilter の基本の書き方

addFilter() は引数を3〜4つ取ります。第1引数がフィルター名、第2引数が名前空間、第3引数が処理をする関数、第4引数が省略可能な優先度(既定は 10)です。

addFilter の形
import { addFilter } from '@wordpress/hooks';

addFilter(
    'blocks.registerBlockType',   // フィルター名
    'myplugin/add-notice',        // 名前空間(プラグイン名/処理名)
    myCallback,                   // 加工する関数
    10                            // 優先度(省略可)
);

名前空間は「誰のどの処理か」を示す識別子で、接頭辞/処理名 の形にします。スラッシュを含まない文字列を渡すとコンソールに警告が出ます。あとから removeFilter( 'blocks.registerBlockType', 'myplugin/add-notice' ) で外したり、他のプラグインとぶつかったときに原因を特定したりするための名前なので、プラグインやテーマの名前を入れておきましょう。

加工する関数は必ず値を返すのがルールです。条件に合わないときは受け取った値をそのまま返します。return を忘れると undefined が次に渡り、対象のブロックがエディターから消えるといった派手な壊れ方をします。

blocks.registerBlockType で属性を追加する

ここからは「段落と見出しに お知らせとして目立たせる スイッチを追加する」例で進めます。まずスイッチの状態を保存する場所、つまり属性を用意します。blocks.registerBlockType フィルターは、ブロックが登録されるたびに設定オブジェクトとブロック名を渡してくれるので、そこに属性を差し込みます。

src/index.js
import { addFilter } from '@wordpress/hooks';

// 対象にするブロック
const TARGET_BLOCKS = [ 'core/paragraph', 'core/heading' ];

function addNoticeAttribute( settings, name ) {
    // 対象外のブロックは何もせずそのまま返す
    if ( ! TARGET_BLOCKS.includes( name ) ) {
        return settings;
    }

    return {
        ...settings,
        attributes: {
            ...settings.attributes,   // 元の属性を残す
            isNotice: {
                type: 'boolean',
                default: false,
            },
        },
    };
}

addFilter(
    'blocks.registerBlockType',
    'myplugin/add-notice-attribute',
    addNoticeAttribute
);

ポイントは2つあります。ひとつは ...settings.attributes元の属性を必ず引き継ぐこと。ここを書き忘れて attributes: { isNotice: ... } だけにすると、段落の本文を保存する content 属性まで消えてしまい、ブロックがまともに動かなくなります。

もうひとつは、名前で対象を絞ることです。このフィルターはすべてのブロックの登録時に呼ばれます。条件を書かないと、コアの数十個のブロックと他のプラグインのブロック全部に属性が増えることになります。

editor.BlockEdit で設定サイドバーに項目を足す

属性を用意しただけでは、値を切り替える手段がありません。編集画面に UI を足すのが editor.BlockEdit フィルターです。このフィルターは値ではなくコンポーネントを受け取ってコンポーネントを返す形(高階コンポーネント)になっていて、元の編集画面を自分の要素で包む書き方をします。

包む処理は @wordpress/composecreateHigherOrderComponent() を使って書きます。第2引数に付ける名前は React DevTools での表示名になるもので、必須ではありませんが付けておくとデバッグが楽になります。

src/index.js(続き)
import { createHigherOrderComponent } from '@wordpress/compose';
import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, ToggleControl } from '@wordpress/components';
import { Fragment } from '@wordpress/element';

const withNoticeControl = createHigherOrderComponent( ( BlockEdit ) => {
    return ( props ) => {
        const { name, attributes, setAttributes, isSelected } = props;

        // 対象外なら元の編集画面をそのまま返す
        if ( ! TARGET_BLOCKS.includes( name ) ) {
            return <BlockEdit { ...props } />;
        }

        return (
            <Fragment>
                <BlockEdit { ...props } />
                { isSelected ? (
                    <InspectorControls>
                        <PanelBody title="お知らせ表示" initialOpen={ false }>
                            <ToggleControl
                                label="お知らせとして目立たせる"
                                checked={ !! attributes.isNotice }
                                onChange={ ( value ) =>
                                    setAttributes( { isNotice: value } )
                                }
                                __nextHasNoMarginBottom
                            />
                        </PanelBody>
                    </InspectorControls>
                ) : null }
            </Fragment>
        );
    };
}, 'withNoticeControl' );

addFilter(
    'editor.BlockEdit',
    'myplugin/with-notice-control',
    withNoticeControl
);

<BlockEdit { ...props } /> が元の編集画面です。これを消さずに描画し、その隣に InspectorControls を並べることで、既存の設定サイドバーに自分のパネルが追加されます。isSelected で囲んでいるのは、選択されていないブロックの分まで InspectorControls を描画しないようにするためです。

ブロック名の判定はフィルターの中で毎回書く

blocks.registerBlockType で属性を足したブロックだけに UI を出したいので、判定条件は両方のフィルターに書きます。片方だけに書くと、属性はあるのに UI が出ない(またはその逆で、属性のないブロックで setAttributes が空振りする)状態になります。この例のように対象のブロック名を定数にまとめておくと、条件がずれる心配がありません。

保存される HTML にクラスを付ける

スイッチを入れたら、フロント側の見た目も変わってほしいところです。段落や見出しのようにエディターで HTML を組み立てて保存する静的ブロックでは、保存時のルート要素に付く属性を blocks.getSaveContent.extraProps で足せます。

src/index.js(続き)
function addNoticeClass( extraProps, blockType, attributes ) {
    if ( ! TARGET_BLOCKS.includes( blockType.name ) ) {
        return extraProps;
    }

    if ( ! attributes.isNotice ) {
        return extraProps;
    }

    return {
        ...extraProps,
        className: [ extraProps.className, 'is-notice' ]
            .filter( Boolean )   // 元のクラスが空のときの undefined を除く
            .join( ' ' ),
    };
}

addFilter(
    'blocks.getSaveContent.extraProps',
    'myplugin/add-notice-class',
    addNoticeClass
);

渡ってくるのは順に「ルート要素に付く props」「ブロックの設定」「そのブロックの属性」です。className を上書きするのではなく、元の値と連結するのを忘れないでください。ここで返したクラスは投稿本文の HTML に文字として保存されるので、あとは通常の CSS で .is-notice にスタイルを当てれば表示が変わります。

なお、このフィルターは save の出力を持つブロックだけが対象です。render.php などで PHP 側が HTML を組み立てる動的ブロックには保存される HTML がないため効きません。その場合は render_block フィルターで出力を加工するか、対象ブロック側の仕組みに合わせた方法を探すことになります。

エディターの中だけ見た目を変えたいとき

保存する HTML は変えず、編集中の表示だけ分かりやすくしたいこともあります。そのときは editor.BlockListBlock を使います。これもコンポーネントを包むフィルターで、エディター上でブロックを囲む要素にだけクラスを足せます。

src/index.js(続き)
const withNoticeEditorClass = createHigherOrderComponent(
    ( BlockListBlock ) => {
        return ( props ) => {
            if ( ! props.attributes.isNotice ) {
                return <BlockListBlock { ...props } />;
            }

            return (
                <BlockListBlock
                    { ...props }
                    className={ [ props.className, 'is-notice-editor' ]
                        .filter( Boolean )
                        .join( ' ' ) }
                />
            );
        };
    },
    'withNoticeEditorClass'
);

addFilter(
    'editor.BlockListBlock',
    'myplugin/with-notice-editor-class',
    withNoticeEditorClass
);

フィルターを書いた JavaScript を読み込む

フィルターは JavaScript なので、エディターの画面で、しかもブロックが登録されるより前に読み込まれている必要があります。enqueue_block_editor_assets アクションで読み込めばこの条件を満たせます。@wordpress/scripts でビルドしているなら、生成される index.asset.php から依存とバージョンを取るのが確実です。

myplugin.php
function myplugin_enqueue_block_filters() {
    $asset_file = plugin_dir_path( __FILE__ ) . 'build/index.asset.php';

    if ( ! file_exists( $asset_file ) ) {
        return;
    }

    // wp-scripts が出力した依存関係とバージョンを使う
    $asset = include $asset_file;

    wp_enqueue_script(
        'myplugin-block-filters',
        plugins_url( 'build/index.js', __FILE__ ),
        $asset['dependencies'],
        $asset['version'],
        true
    );
}
add_action( 'enqueue_block_editor_assets', 'myplugin_enqueue_block_filters' );

フロント側の見た目に使う CSS は、これとは別に wp_enqueue_scripts で読み込みます。エディター内の表示にも同じスタイルを効かせたい場合は enqueue_block_assets を使うと、エディターとフロントの両方に読み込まれます。

フィルターが効かないときに見るところ

設定項目もクラスも一切出てこない

まずビルドが済んでいるか確認します。src/index.js を書き換えただけでは反映されず、npm run startnpm run build を通した build/index.js が読み込まれます。次に、ブラウザーのコンソールで名前空間の警告やエラーが出ていないかを見ます。JSX を使っているのに @wordpress/element 相当の設定が足りないと、ビルドの段階でエラーになります。

スクリプトが enqueue_block_editor_assets ではなく admin_enqueue_scripts などで読み込まれていると、ブロックの登録より後に実行されて blocks.registerBlockType が空振りします。エディター用のフックで読み込んでいるか確かめてください。

スイッチは出るが、保存すると元に戻る

属性の追加が効いていないパターンです。blocks.registerBlockType フィルターで対象のブロック名がずれていないかを確認します。ブロック名は core/paragraph のように名前空間付きで、paragraph だけでは一致しません。判定に使っている定数の中身を console.log() で出してみるのが早いです。

属性は追加できているのに保存後に消える場合は、値の保存先がありません。属性の定義に source を書かなければ値はブロックコメントの JSON に入るので、通常は保存されます。source: 'attribute' のように HTML から読む指定を足していると、対応する要素や属性がないと読み戻せず既定値に戻ってしまいます。

既存の投稿で「このブロックには意図しない、または無効なコンテンツが含まれています」と出る

blocks.getSaveContent.extraProps は保存される HTML を変えるフィルターなので、すでに保存済みの投稿の HTML と、いまのコードが生成する HTML がずれるとブロック検証エラーになります。多いのは、クラス名を後から変更したケースです。

コアブロックに対するフィルターでは自分で deprecated を足せないため、クラス名は最初に決めて変えないのが基本方針になります。どうしても変更が必要なときは、旧クラスも受け付けるよう CSS 側で両方に対応させるか、投稿本文を一括置換する方向で考えます。属性を追加するだけ、UI を足すだけのフィルターであれば保存される HTML は変わらないので、この問題は起きません。

PHP 側で属性が取れない

JavaScript のフィルターで追加した属性は、PHP 側のブロック定義には登録されていません。そのため render_block などで属性を参照しようとしても、既定値の補完が行われず値が見えないことがあります。サーバー側でも属性を認識させたいときは、PHP の register_block_type_args フィルターで同じ属性を足しておきます。

myplugin.php
function myplugin_register_notice_attribute( $args, $block_type ) {
    $targets = array( 'core/paragraph', 'core/heading' );

    if ( ! in_array( $block_type, $targets, true ) ) {
        return $args;
    }

    if ( ! isset( $args['attributes'] ) ) {
        $args['attributes'] = array();
    }

    $args['attributes']['isNotice'] = array(
        'type'    => 'boolean',
        'default' => false,
    );

    return $args;
}
add_filter( 'register_block_type_args', 'myplugin_register_notice_attribute', 10, 2 );

JavaScript 側と PHP 側で属性の型や既定値をそろえておくのが大事です。片方が boolean、片方が string のようにずれていると、値の解釈が食い違って原因の分かりにくい不具合になります。

まとめ

ブロックのフィルターは、既存のブロックを作り直さずに機能を足すための仕組みです。@wordpress/hooksaddFilter() に「フィルター名・名前空間・処理する関数」を渡して登録し、関数の中では対象のブロック名で絞り込み、受け取った値を必ず返すのが共通のルールです。

役割分担は、値の置き場所を作るのが blocks.registerBlockType、それを操作する UI を足すのが editor.BlockEdit、結果をフロントの HTML に出すのが blocks.getSaveContent.extraProps、編集中の見た目だけ変えるのが editor.BlockListBlock です。この4つを組み合わせれば、コアブロックに独自の設定をひとつ追加する、という定番のカスタマイズはひととおり書けます。

気をつけたいのは、対象を絞る条件と、既存の値を引き継ぐスプレッド構文、そして保存される HTML を変えるときの検証エラーです。まずは属性と UI の追加だけから始めて、動作を確認しながら保存側に手を伸ばしていくと、投稿を壊さずに進められます。

参考ページ