カスタムブロックを作っていると、「枠だけ自分で用意して、中身は見出しや段落など既存のブロックを自由に置けるようにしたい」という場面が出てきます。これを実現するのが @wordpress/block-editor の InnerBlocks です。この記事では edit 側の <InnerBlocks /> と save 側の <InnerBlocks.Content /> という基本形から、ラッパー要素を1つにまとめられる useInnerBlocksProps、そして入れ子にできるブロックを制御する allowedBlocks / template / templateLock / orientation といった props までを解説します。WordPress 6 系、apiVersion 3 を前提としています。
目次
InnerBlocks はカスタムブロックを「入れ物」にするコンポーネント
RichText でテキストを編集できるブロックは作れても、それだけでは「中に画像も見出しも段落も置けるカード」のようなブロックは作れません。中身の種類を自分で全部実装することになってしまうからです。InnerBlocks を使うと、その部分をブロックエディターそのものに任せられます。カスタムブロックの中にブロック挿入ボタンが現れ、読者ではなく編集者が好きなブロックを積んでいけるようになります。
身近な例では、コアブロックの「グループ」「カラム」「カバー」がこの仕組みで動いています。グループブロック自体は div を1枚出しているだけで、中に何を入れるかは InnerBlocks が受け持っています。自作のブロックでも同じことができる、と考えると分かりやすいでしょう。
入れ子になったブロックは、親ブロックの属性としてではなく独立したブロックの配列として扱われます。保存される HTML も、親ブロックのブロックコメントの内側に子ブロックのブロックコメントがそのまま入る形になります。この「子は子のまま保存される」という点が、あとで触れるトラブルの理解にも効いてきます。
基本の使い方|edit に InnerBlocks、save に InnerBlocks.Content
最小構成は驚くほど単純です。edit の中に <InnerBlocks /> を、save の中に <InnerBlocks.Content /> を置くだけで、そのブロックは入れ物になります。ここでは webool/card という枠だけのブロックを例にします。
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "webool/card",
"title": "カード",
"category": "design",
"icon": "index-card",
"supports": {
"html": false
},
"textdomain": "webool",
"editorScript": "file:./index.js",
"style": "file:./style-index.css"
}
block.json 側に入れ子のための特別な項目は要りません。attributes も、子ブロックを保持するための定義は不要です。中身はブロックとして保存されるので、親が属性で抱える必要がないからです。
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
export default function Edit() {
const blockProps = useBlockProps( { className: 'card' } );
return (
<div { ...blockProps }>
<div className="card__body">
{/* ここが子ブロックを置ける領域になる */}
<InnerBlocks />
</div>
</div>
);
}
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
export default function save() {
const blockProps = useBlockProps.save( { className: 'card' } );
return (
<div { ...blockProps }>
<div className="card__body">
{/* 子ブロックの HTML がここに書き出される */}
<InnerBlocks.Content />
</div>
</div>
);
}
edit と save で名前が違うところがポイントです。edit の <InnerBlocks /> は編集用の UIで、子ブロックのリストと挿入ボタン(インサーター)を描画します。save の <InnerBlocks.Content /> は保存用のプレースホルダーで、子ブロックが出力した HTML をその位置に差し込むだけの役割です。useBlockProps() と useBlockProps.save() の関係とちょうど同じ、編集用と保存用の対になっていると考えると覚えやすいでしょう。
保存される HTML はどうなるか
このブロックの中に段落ブロックを1つ入れて保存すると、投稿本文には次のような形で記録されます。
<!-- wp:webool/card -->
<div class="wp-block-webool-card card"><div class="card__body">
<!-- wp:paragraph -->
<p>カードの中の文章です。</p>
<!-- /wp:paragraph -->
</div></div>
<!-- /wp:webool/card -->
親ブロックのコメントの内側に、子ブロックのコメントがそのまま入っています。投稿を開き直したときは、WordPress がこのコメントを読んで子ブロックを復元し、親ブロックの save が返すマークアップと突き合わせて検証します。子ブロックの中身がどれだけ変わっても親の検証には影響しないので、入れ子の部分は比較的トラブルが起きにくい作りになっています。
useInnerBlocksProps でラッパー要素を減らす
上の例では、useBlockProps を付けた外側の div と、InnerBlocks を包む内側の div の2枚構造になっていました。「ブロックのルート要素がそのまま子ブロックの入れ物であってほしい」というときは、useInnerBlocksProps を使うと1枚にまとめられます。
useInnerBlocksProps は第1引数に useBlockProps() の返り値を、第2引数に InnerBlocks に渡していた設定を受け取り、両方をマージしたオブジェクトを返します。返り値には子ブロックの描画結果が children として含まれているので、要素にスプレッドするだけで入れ子が成立します。
import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
const ALLOWED_BLOCKS = [ 'core/heading', 'core/paragraph', 'core/image' ];
export default function Edit() {
const blockProps = useBlockProps( { className: 'card' } );
// ブロックのルート要素をそのまま子ブロックの入れ物にする
const innerBlocksProps = useInnerBlocksProps( blockProps, {
allowedBlocks: ALLOWED_BLOCKS,
} );
// innerBlocksProps.children に子ブロックが入っているので自己終了タグでよい
return <div { ...innerBlocksProps } />;
}
import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
export default function save() {
const blockProps = useBlockProps.save( { className: 'card' } );
const innerBlocksProps = useInnerBlocksProps.save( blockProps );
return <div { ...innerBlocksProps } />;
}
save 側は useInnerBlocksProps.save() を使います。こちらも useBlockProps.save() の返り値を渡す形で、InnerBlocks.Content を自分で書く必要がなくなります。allowedBlocks のような編集時の設定は保存には関係しないので、save 側では第2引数を省略して構いません。
どちらの書き方を選ぶかは、子ブロックを直接ルート要素に入れたいかどうかで決めます。カードの見出し部分と本文部分を分けたい、装飾用の要素を挟みたいといった場合は <InnerBlocks /> を任意の位置に置ける前者が向いています。余計な div を出したくない、フレックスやグリッドをルート要素に直接当てたい場合は useInnerBlocksProps のほうがすっきりします。
入れ子を制御する主な props
InnerBlocks は何も指定しなければ「どんなブロックでも自由に入れられる空の領域」になります。実際のブロック開発では、入れられるブロックを絞ったり、最初から中身を用意しておいたりしたい場面がほとんどです。よく使う props を整理します。ここに挙げた props は、<InnerBlocks ... /> の属性として渡しても、useInnerBlocksProps の第2引数のオブジェクトに書いても同じように働きます。
| prop | 指定する値 | 効果 |
|---|---|---|
allowedBlocks | ブロック名の配列(例: [ 'core/heading', 'core/paragraph' ]) | この中に直接挿入できるブロックを、指定したものだけに限定する |
template | [ [ ブロック名, 属性, 子ブロック ] ] の配列 | ブロックを挿入した直後の初期状態を決める。属性と子ブロックは省略可 |
templateLock | 'all' / 'insert' / 'contentOnly' / false | 子ブロックの追加・削除・並べ替えをどこまで許すかを決める |
orientation | 'horizontal' / 'vertical'(既定は縦) | 子ブロックの並びの向きをエディターに伝える。ドラッグ時の挿入位置の表示やキーボード操作の向きが変わる |
renderAppender | false / InnerBlocks.ButtonBlockAppender / InnerBlocks.DefaultBlockAppender / 自作コンポーネント | 末尾に表示されるブロック追加ボタン(アペンダー)の見た目を差し替える。false で非表示にできる |
allowedBlocks で中に入れられるブロックを絞る
allowedBlocks にブロック名の配列を渡すと、そのブロックの直下に挿入できるブロックが限定されます。インサーターの一覧にも、許可したブロックだけが並ぶようになります。「このカードには見出しと段落と画像しか置かせたくない」といった運用ルールを、注意書きではなく仕組みとして表現できるのが利点です。
効くのは直下の階層だけである点には注意してください。許可したブロックがさらに InnerBlocks を持っている場合、その内側で何を入れられるかはそのブロック自身の設定に従います。逆に「このブロックはあの親の中でしか使えない」という制限を子側から掛けたいときは、子ブロックの block.json で parent を指定します。この2つは向きが逆なだけで、組み合わせて使うと親子関係をきっちり固定できます。
なお、配列は毎回の描画で作り直すと不要な再レンダリングにつながるため、コンポーネントの外で定数として定義しておくのが定番です。先ほどのコード例で ALLOWED_BLOCKS をファイルの先頭に置いていたのはそのためです。
template で初期状態を用意する
template は、ブロックを挿入した瞬間に中に配置しておく子ブロックの並びです。1要素が [ ブロック名, 属性のオブジェクト, さらに内側のテンプレート ] という配列になっていて、後ろの2つは省略できます。空のカードから始めるより、見出しと段落が最初から入っているほうが編集者は迷いません。
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
const TEMPLATE = [
[ 'core/heading', { level: 3, placeholder: 'カードの見出し' } ],
[ 'core/paragraph', { placeholder: '説明文を入力' } ],
];
export default function Edit() {
const blockProps = useBlockProps( { className: 'card' } );
return (
<div { ...blockProps }>
<InnerBlocks template={ TEMPLATE } />
</div>
);
}
3番目の要素にさらにテンプレートの配列を書けば、入れ子のテンプレートも作れます。たとえばカラムブロックを2列で初期配置する、といった指定が可能です。placeholder のように編集画面での案内に使える属性を混ぜておくと、何を書けばよいかが伝わりやすくなります。
template はあくまで初期状態であり、そのままでは編集者が自由に消したり足したりできます。「この構成から動かしてほしくない」場合に組み合わせるのが、次の templateLock です。
templateLock で構成を固定する
templateLock は、子ブロックの操作をどこまで許すかを決める設定です。値によって制限の強さが変わります。
| 値 | できなくなること |
|---|---|
'all' | 子ブロックの追加・削除・並べ替えのすべてが禁止される。テンプレートで決めた構成が固定される |
'insert' | 追加と削除が禁止される。並べ替えは可能 |
'contentOnly' | ブロックの構造には触れず、テキストや画像などの中身の編集だけができる状態になる |
false | ロックを掛けない。親から継承されるロックを打ち消したいときに明示的に指定する |
template と templateLock: 'all' を組み合わせると、「見出しと段落が必ずこの順で入っているカード」を作れます。サイト全体で見た目を揃えたい、編集者が構造を崩さないようにしたい、といった要件に向いた指定です。
templateLock を指定しなかった場合、ロックの状態は親から引き継がれます。自分のブロックには何も書いていないのに子ブロックを追加できない、というときは、そのブロックが置かれている親側のロックを疑ってみてください。継承を断ち切りたいときは templateLock={ false } と明示的に書きます。
orientation は見た目ではなくエディターへの申告
orientation に 'horizontal' を指定すると、子ブロックが横に並んでいるものとしてエディターが振る舞います。ドラッグ中に表示される挿入位置のインジケーターが縦線になり、矢印キーでの移動やツールバーの移動ボタンの向きも横並びに合わせたものになります。
誤解しやすいのは、この props 自体は子ブロックを横に並べてくれないという点です。実際に横並びにするのは CSS(display: flex など)や supports.layout の役割で、orientation はその見た目に合わせて操作感を揃えるための申告にあたります。横並びのレイアウトを作ったのに矢印キーの動きが縦のままで違和感がある、というときに指定するものだと考えてください。
動的ブロックでは入れ子の HTML が $content に渡る
save で null を返し、フロント側の出力を PHP に任せる動的ブロックでも InnerBlocks は使えます。この場合、子ブロックが出力した HTML は render に指定した PHP ファイルの $content に渡ってきます。
<?php
/**
* $attributes … ブロックの属性
* $content … 子ブロック(InnerBlocks)が出力した HTML
* $block … WP_Block インスタンス
*/
?>
<div <?php echo get_block_wrapper_attributes( array( 'class' => 'card' ) ); ?>>
<div class="card__body">
<?php echo $content; ?>
</div>
</div>
$content は子ブロックがすでにレンダリングを終えた HTML なので、そのまま出力すれば入れ子の内容が表示されます。動的ブロックにすると save のマークアップを持たないぶん、あとから枠の HTML を変えてもブロック検証エラーが起きないという利点もあります。編集画面側は静的ブロックと同じく <InnerBlocks /> または useInnerBlocksProps を使って組み立てます。
入れ子のブロックが保存されない・追加できないとき
InnerBlocks まわりでつまずくポイントは、原因のパターンがある程度決まっています。症状ごとに見ていきます。
投稿を開き直すと中身が消えている
編集中は子ブロックが入っているのに、保存して開き直すと空になっている場合、まず save に <InnerBlocks.Content /> を書いたか確認してください。edit にだけ <InnerBlocks /> を置いて save を書き忘れると、子ブロックの HTML を書き出す場所がないため、保存された本文に中身が残りません。useInnerBlocksProps を使っている場合は、save 側で useInnerBlocksProps.save() を呼んでいるかが同じチェックポイントになります。
また、save のマークアップを途中で変更したときは、既存の投稿でブロック検証エラーが出ます。エディターの「ブロックのリカバリーを試行」で作り直せますが、リカバリーの過程で入れ子の構造が失われることもあるため、公開済みの記事に使っているブロックでは deprecated を用意して移行するほうが安全です。
ブロック追加ボタンが出てこない
アペンダーが表示されないときに疑うのは、templateLock と renderAppender です。templateLock が 'all' や 'insert' になっていれば追加は禁止されるので、ボタンも出ません。自分で書いた覚えがなければ、前述のとおり親ブロックから継承されている可能性があります。renderAppender={ false } を指定した場合も当然表示されません。
allowedBlocks の指定ミスも見落としがちです。ブロック名は core/paragraph のように名前空間から書く必要があり、paragraph だけでは一致しません。許可したブロックが1つも存在しない状態になると、インサーターに出せるものがなくなります。
2つ目の InnerBlocks が動かない
「ヘッダー用とフッター用で入れ子の領域を2つ持ちたい」と考えて <InnerBlocks /> を2つ並べても、思ったようには動きません。1つのブロックが持てる入れ子の領域は1つだけという制約があるためです。子ブロックは親に対して1本のリストとして紐付いているので、領域を分けて別々に管理することはできません。
この要件を満たしたいときは、領域ごとにブロックを分ける設計にします。「カード」ブロックの template に「カードヘッダー」ブロックと「カード本文」ブロックを並べ、それぞれが自分の InnerBlocks を持つ形です。子側の block.json に parent を書いてカードの中でしか使えないようにしておけば、単体で挿入されて崩れる心配もありません。
フロントで枠のスタイルが当たらない
編集画面では枠が見えているのにフロントでは効いていない場合、save のルート要素に useBlockProps.save() を展開し忘れていないか確認してください。wp-block-… のクラスが出力されないため、そのクラスを前提にした CSS が当たりません。InnerBlocks を使うブロックは save の中身が薄くなりがちで、ルート要素の属性を素で書いてしまう事故が起きやすい部分です。
スタイルの読み込み先にも気を配りましょう。block.json の editorStyle はエディターだけ、style はエディターとフロントの両方で読み込まれます。枠のレイアウトはフロントでも必要なので style に、編集中の点線ガイドのような補助的な装飾は editorStyle に、と分けて書くのが基本です。
まとめ
InnerBlocks は、カスタムブロックを他のブロックの入れ物にするためのコンポーネントです。edit に <InnerBlocks />、save に <InnerBlocks.Content /> を置くのが基本形で、この2つが揃って初めて入れ子の内容が投稿に保存されます。ルート要素をそのまま入れ物にしたいときは useInnerBlocksProps(保存側は useInnerBlocksProps.save())を使うと、余計なラッパー要素を出さずに済みます。
中身の自由度は props で調整します。allowedBlocks で挿入できるブロックを絞り、template で初期構成を用意し、templateLock でその構成をどこまで固定するかを決めます。orientation はレイアウトそのものではなく、エディターの操作感を並びの向きに合わせるための指定です。動的ブロックにする場合は、子ブロックの HTML が PHP 側の $content に渡ってきます。中身が保存されないときは save 側の書き忘れ、追加ボタンが出ないときは templateLock の継承を疑う——この2点を押さえておけば、多くのつまずきは自力で解決できるはずです。