カスタムブロックの設定サイドバーには、オン・オフのスイッチやプルダウンのように「決まった候補から選ばせる」部品のほかに、編集者が自由に文字を打ち込む入力欄も置けます。見出しの文言、リンク先の URL、カードに添える説明文といった値は、選択肢では表現できないからです。
この記事では @wordpress/components の TextControl(1行の入力欄)・TextareaControl(複数行の入力欄)・CheckboxControl(チェックボックス)の3つを取り上げます。それぞれの props の意味から、block.json の属性定義・edit.js での組み込み・save.js での出力までを1本のブロックとして通しで作ります。カスタムブロック開発を始めたばかりの方に向けた内容です。
目次
自由入力の値は選択式のコントロールでは扱えない
ブロックの設定でよく使う ToggleControl・SelectControl・RangeControl は、いずれも「あらかじめ用意した範囲のなかから値を決める」部品です。オンかオフか、用意した選択肢のどれか、最小値と最大値のあいだの数値か。どれも作り手が先に候補を決めておく必要があります。
ところが実際のブロックでは、候補を決められない値のほうが多くあります。ボタンに載せる文字、リンク先の URL、図版のキャプション、カードの説明文。こうした編集者が自由に決める文字列を受け取るのが TextControl と TextareaControl です。そして CheckboxControl は真偽値を扱う部品で、スイッチではなく従来どおりのチェックボックスとして見せたいときに使います。
置き場所はどれも共通で、@wordpress/block-editor の InspectorControls の内側、さらに PanelBody で囲んだ中です。InspectorControls が右サイドバーへの差し込み口、PanelBody が開閉できるグループの枠、その中身に入力欄を並べていく、という構成になります。
3つのコントロールの共通点と違い
3つとも @wordpress/components からインポートし、「ラベル」「現在の値」「値が変わったときに呼ばれる関数」を渡す、という書き方は共通です。違うのは現在の値を渡す prop 名と、onChange に渡ってくる値の型です。
| コントロール | 対応する属性の型 | 現在の値を渡す prop | onChange に渡ってくる値 | 向いている設定 |
|---|---|---|---|---|
TextControl | string | value | 入力後の文字列 | 見出し・URL・短いラベルなど1行で収まる値 |
TextareaControl | string | value | 入力後の文字列(改行を含む) | 説明文・注意書きなど複数行になる値 |
CheckboxControl | boolean | checked | 切り替え後の true / false | 「新しいタブで開く」のような有効・無効の設定 |
インポートはまとめて書けます。InspectorControls だけパッケージが違う点に注意してください。
import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import {
PanelBody,
TextControl,
TextareaControl,
CheckboxControl,
} from '@wordpress/components';
WordPress 6.7 以降で付けておきたい props
@wordpress/components のコントロールは、見た目を新しい設計に合わせる作業が段階的に進められています。その新しいスタイルを先に有効にするための prop が __next40pxDefaultSize と __nextHasNoMarginBottom です。付けずに使うと、WordPress 6.7 以降のブロックエディターではブラウザーのコンソールに「新しい既定値へ移行してほしい」という趣旨の非推奨(deprecated)警告が出ます。
| prop | 何が変わるか | 今回の3つでの扱い |
|---|---|---|
__next40pxDefaultSize | 入力欄の高さが新しい既定サイズ(40px)になる | TextControl で指定する |
__nextHasNoMarginBottom | コントロールが自前で持っていた下方向のマージンがなくなる | 3つとも指定する |
__next40pxDefaultSize は入力欄の高さに関する prop なので、テキストエリアやチェックボックスには関係しません。TextareaControl と CheckboxControl には __nextHasNoMarginBottom だけを付けます。どちらも真偽値を取る prop ですが、JSX では prop 名だけを書けば true を渡したことになるので、以降のコード例のように値を省略して書くのが一般的です。
TextControl — 1行の文字列を受け取る
TextControl は input 要素を描画します。組み合わせる属性は type: "string" です。
"attributes": {
"buttonLabel": {
"type": "string",
"default": "資料をダウンロード"
}
}
<TextControl
__next40pxDefaultSize
__nextHasNoMarginBottom
label={ __( 'ボタンの文言', 'my-blocks' ) }
value={ buttonLabel }
placeholder={ __( '例:資料をダウンロード', 'my-blocks' ) }
onChange={ ( value ) => setAttributes( { buttonLabel: value } ) }
/>
ここで一番の注意点は onChange です。素の input を React で扱うときは onChange={ ( event ) => ... event.target.value } と書きますが、TextControl の onChange には入力後の文字列そのものが渡ってきます。イベントオブジェクトではありません。event.target.value と書くと undefined になり、何を打っても属性が空のままになります。
主な props は次のとおりです。
| prop | 説明 |
|---|---|
label | 入力欄の上に表示される名前。省略せずに付ける |
value | 現在の値(文字列)。属性の値をそのまま渡す |
onChange | 値が変わるたびに呼ばれる。引数は入力後の文字列 |
type | input の type。既定は "text"。"url"・"email"・"number" なども指定できる |
placeholder | 未入力のときに薄く表示するヒント |
help | 入力欄の下に小さく表示する補足説明 |
disabled | true で入力できなくする |
hideLabelFromVision | true でラベルを視覚的に隠す。スクリーンリーダーには読まれる |
className | 外側の要素に追加するクラス名 |
__next40pxDefaultSize | 新しい既定サイズ(高さ 40px)を有効にする |
__nextHasNoMarginBottom | 下マージンを持たない新しいスタイルを有効にする |
ここに挙げていない props(maxLength や autoComplete など)は、内部の input 要素にそのまま渡されます。入力できる文字数を制限したいときなどは、HTML の属性をそのまま書けば効きます。
type を変えても受け取る値は文字列のまま
type="number" を指定すると、入力欄にスピンボタンが付いて数字が打ちやすくなります。ただしこれは input 要素の見た目と入力補助が変わるだけで、onChange に渡ってくるのは相変わらず文字列です。"12" のような文字列が属性に入るため、そのまま計算に使うと文字列連結になってしまいます。
<TextControl
__next40pxDefaultSize
__nextHasNoMarginBottom
type="number"
label={ __( '表示件数', 'my-blocks' ) }
value={ postsToShow }
onChange={ ( value ) =>
// 属性が type: "number" なら数値に変換してから渡す
setAttributes( { postsToShow: parseInt( value, 10 ) || 0 } )
}
/>
parseInt() は空文字を渡されると NaN を返すので、|| 0 のような受け皿を用意しておくと、入力欄を空にした瞬間にブロックが壊れるのを防げます。
TextareaControl — 複数行のテキストを受け取る
TextareaControl は textarea 要素を描画します。使い方は TextControl とほぼ同じで、value と onChange の組み合わせも変わりません。違いは rows で高さ(表示する行数)を指定できる点と、受け取る文字列に改行(\n)が含まれる点です。
<TextareaControl
__nextHasNoMarginBottom
label={ __( '説明文', 'my-blocks' ) }
value={ description }
rows={ 6 }
help={ __( 'カードの中に表示される短い説明です。', 'my-blocks' ) }
onChange={ ( value ) => setAttributes( { description: value } ) }
/>
| prop | 説明 |
|---|---|
label | 入力欄の上に表示される名前 |
value | 現在の値(文字列) |
onChange | 値が変わるたびに呼ばれる。引数は入力後の文字列 |
rows | 表示する行数。指定しない場合は 4 行 |
placeholder | 未入力のときに薄く表示するヒント |
help | 入力欄の下に小さく表示する補足説明 |
disabled | true で入力できなくする |
hideLabelFromVision | true でラベルを視覚的に隠す |
__nextHasNoMarginBottom | 下マージンを持たない新しいスタイルを有効にする |
rows はあくまで初期の高さです。ブラウザーの標準機能で右下をドラッグすれば編集者が広げられるので、長文を書かせる可能性があるなら少し大きめの値を入れておくと親切です。
なお、装飾(太字やリンク)を含む本文を書かせたい場合は TextareaControl ではなく、ブロック本体に RichText を置くほうが適しています。TextareaControl が向いているのは、あくまで装飾のいらない素のテキストです。
CheckboxControl — 真偽値をチェックボックスで切り替える
CheckboxControl は、四角いチェックボックスとその横のラベルを描画します。現在の状態を渡す prop は value ではなく checked で、onChange には切り替え後の真偽値がそのまま渡ってきます。
<CheckboxControl
__nextHasNoMarginBottom
label={ __( '新しいタブで開く', 'my-blocks' ) }
help={ __( 'PDF など、ページを離れずに開かせたいときにチェックします。', 'my-blocks' ) }
checked={ openInNewTab }
onChange={ ( value ) => setAttributes( { openInNewTab: value } ) }
/>
| prop | 説明 |
|---|---|
label | チェックボックスの横に表示される文字 |
checked | チェックされているかどうか(真偽値) |
onChange | 切り替え時に呼ばれる。引数は切り替え後の真偽値 |
help | 下に小さく表示する補足説明 |
indeterminate | true でチェックとも未チェックともつかない中間状態の見た目にする |
disabled | true で操作できなくする |
className | 外側の要素に追加するクラス名 |
__nextHasNoMarginBottom | 下マージンを持たない新しいスタイルを有効にする |
indeterminate は、たとえば「すべて選択」のチェックボックスで、子項目が一部だけ選ばれている状態を表すためのものです。値としてチェックされているわけではなく、見た目だけが中間状態になる点に注意してください。実際に保存される値は checked のほうです。
ToggleControl との使い分け
真偽値を切り替える部品としては ToggleControl(スイッチ)もあり、props の形もほとんど同じです。目安としては、切り替えた瞬間にブロックの見た目が変わる設定はスイッチ、記事を保存したあとの挙動が変わる設定はチェックボックスと考えると迷いません。コア(WordPress 本体)のブロックでも、表示・非表示の切り替えはスイッチ、複数の条件を並べて選ばせる場面ではチェックボックス、という使い分けが見られます。
選択肢が3つ以上あってラジオボタンで見せたい場合は RadioControl が使えます。こちらは options に { label, value } の配列を渡し、現在の値は value ではなく selected で渡す点が独特です。onChange には選ばれた value(文字列)が渡ってきます。
1本のブロックに組み込む
ここまでの3つをまとめて、「ダウンロードボックス」ブロックを作ってみます。タイトルと URL を TextControl、説明文を TextareaControl、リンクを新しいタブで開くかどうかを CheckboxControl で設定する構成です。
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "webool/download-box",
"title": "ダウンロードボックス",
"category": "design",
"icon": "download",
"attributes": {
"title": {
"type": "string",
"default": ""
},
"description": {
"type": "string",
"default": ""
},
"fileUrl": {
"type": "string",
"default": ""
},
"openInNewTab": {
"type": "boolean",
"default": false
}
},
"editorScript": "file:./index.js",
"style": "file:./style-index.css"
}
文字列の属性には "default": "" を入れておきます。これを忘れると初期値が undefined になり、後述する React の警告が出ます。
import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import {
PanelBody,
TextControl,
TextareaControl,
CheckboxControl,
} from '@wordpress/components';
export default function Edit( { attributes, setAttributes } ) {
const { title, description, fileUrl, openInNewTab } = attributes;
const blockProps = useBlockProps( { className: 'download-box' } );
return (
<>
<InspectorControls>
<PanelBody title={ __( 'ダウンロード設定', 'my-blocks' ) }>
<TextControl
__next40pxDefaultSize
__nextHasNoMarginBottom
label={ __( 'タイトル', 'my-blocks' ) }
value={ title }
placeholder={ __( '例:料金表(PDF)', 'my-blocks' ) }
onChange={ ( value ) =>
setAttributes( { title: value } )
}
/>
<TextControl
__next40pxDefaultSize
__nextHasNoMarginBottom
type="url"
label={ __( 'ファイルの URL', 'my-blocks' ) }
value={ fileUrl }
help={ __( 'メディアライブラリからコピーした URL を貼り付けます。', 'my-blocks' ) }
onChange={ ( value ) =>
setAttributes( { fileUrl: value } )
}
/>
<TextareaControl
__nextHasNoMarginBottom
label={ __( '説明文', 'my-blocks' ) }
value={ description }
rows={ 5 }
onChange={ ( value ) =>
setAttributes( { description: value } )
}
/>
<CheckboxControl
__nextHasNoMarginBottom
label={ __( '新しいタブで開く', 'my-blocks' ) }
checked={ openInNewTab }
onChange={ ( value ) =>
setAttributes( { openInNewTab: value } )
}
/>
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
<p className="download-box__title">
{ title || __( '(タイトル未設定)', 'my-blocks' ) }
</p>
<p className="download-box__description">{ description }</p>
</div>
</>
);
}
InspectorControls とブロック本体は並列に置くので、全体を空タグ(フラグメント)で囲んでいます。InspectorControls の中身はエディターの右サイドバーに描画されるだけで、その場には何も表示されません。
タイトルが空のときに title || '(タイトル未設定)' と代替のテキストを出しているのは、エディター上でブロックの存在が分からなくなるのを防ぐためです。保存側にはこの代替テキストを出さないので、公開ページには影響しません。
import { useBlockProps } from '@wordpress/block-editor';
export default function save( { attributes } ) {
const { title, description, fileUrl, openInNewTab } = attributes;
const blockProps = useBlockProps.save( { className: 'download-box' } );
return (
<div { ...blockProps }>
<p className="download-box__title">{ title }</p>
{ description && (
<p className="download-box__description">{ description }</p>
) }
<a
className="download-box__link"
href={ fileUrl }
target={ openInNewTab ? '_blank' : undefined }
rel={ openInNewTab ? 'noopener noreferrer' : undefined }
>
ダウンロード
</a>
</div>
);
}
チェックボックスの値を HTML に反映するときのコツが undefined の使い方です。target={ openInNewTab ? '_blank' : '' } と空文字を渡すと target="" という無意味な属性が残りますが、undefined を渡すと属性そのものが出力されません。真偽値で属性の有無を切り替えたいときは undefined を使ってください。
説明文のほうは { description && ( ... ) } で、値が空のときは段落ごと出力しないようにしています。空の <p> が残ると余白だけが生まれてしまうためです。
入力した値が反映されないときに見るところ
何を打っても入力欄が空のまま
まず onChange の書き方を確認してください。前述のとおり TextControl と TextareaControl の onChange には文字列が直接渡ってきます。( event ) => setAttributes( { title: event.target.value } ) と書いていると、event の中身は文字列なので target が存在せず、結果として undefined が属性に入ります。入力欄は value に undefined を渡されるため空のまま、という症状になります。
CheckboxControl で同じ症状(クリックしてもチェックが付かない)が出る場合は、checked ではなく value で状態を渡していないかを見てください。value は CheckboxControl が見ている prop ではないので、状態が反映されません。
コンソールに「uncontrolled input を controlled に変えた」と出る
「A component is changing an uncontrolled input to be controlled」という React の警告です。value に最初 undefined が渡り、入力後に文字列が渡ったときに出ます。原因は block.json の属性に default を書いていないことがほとんどです。
文字列の属性には "default": ""、真偽値の属性には "default": false を必ず書いておきます。既存の投稿に古いブロックが埋まっていて属性が欠けている可能性もあるので、心配なら value={ title ?? '' } のように受け側でも空文字に寄せておくと確実です。
説明文の改行が公開ページで消える
TextareaControl で改行を入れても、公開ページでは1行につながって見えることがあります。属性には改行文字(\n)がきちんと保存されているのですが、HTML では連続する空白や改行がまとめて1つの空白として表示されるためです。
一番手軽な対処は CSS で white-space: pre-line を当てることです。改行文字がそのまま改行として表示され、余計な行頭の空白は詰めてくれます。
.download-box__description {
white-space: pre-line;
}
段落として <p> を分けたい場合は、save の中で description.split( '\n' ) して配列を回す方法もあります。ただしその場合は各要素に key が必要になり、あとから save の出力を変えるとブロック検証エラー(Block validation failed)の原因にもなります。まずは CSS で済ませるほうが安全です。
save を書き換えたあとに既存の投稿が壊れる
コントロールを追加して save の出力を変えると、すでに保存されている投稿の HTML と save が生成する HTML が食い違い、「このブロックには、想定されていないか無効なコンテンツが含まれています」という表示になります。属性を増やすこと自体は問題ありませんが、save が出力するマークアップを変えたときは必ず起きるので、公開済みのブロックを改修するときは deprecated に旧バージョンの save を残しておくのが正攻法です。開発中でまだ誰も使っていないブロックなら、投稿側のブロックを一度削除して入れ直すのが手早いでしょう。
まとめ
TextControl は1行の文字列、TextareaControl は複数行の文字列、CheckboxControl は真偽値を、それぞれ設定サイドバーから受け取るためのコントロールです。どれも InspectorControls と PanelBody の中に置き、属性の値を渡して onChange で setAttributes() を呼ぶ、という形は共通しています。
つまずきやすいのは3点です。現在の値を渡す prop がテキスト系は value、チェックボックスは checked であること。onChange にはイベントではなく値そのものが渡ってくること。そして block.json の属性に default を書いておくこと。この3つを押さえておけば、あとは save でどう出力するかを決めるだけで、編集者が自由に値を入れられるブロックが作れます。